001/*
002 * ModeShape (http://www.modeshape.org)
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 *       http://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 */
016package org.modeshape.common.util;
017
018import java.io.BufferedInputStream;
019import java.io.File;
020import java.io.FileInputStream;
021import java.io.IOException;
022import java.io.InputStream;
023import java.io.Reader;
024import java.nio.CharBuffer;
025import java.nio.charset.Charset;
026import java.nio.charset.CharsetEncoder;
027import java.security.MessageDigest;
028import java.security.NoSuchAlgorithmException;
029import org.modeshape.common.SystemFailureException;
030import org.modeshape.common.annotation.Immutable;
031
032/**
033 * A simple utility to generate various kinds of secure hashes.
034 */
035@Immutable
036public class SecureHash {
037
038    /**
039     * Commonly-used hashing algorithms.
040     */
041    @Immutable
042    public enum Algorithm {
043        MD2("MD2", 128, "The MD2 message digest algorithm as defined in RFC 1319"),
044        MD5("MD5", 128, "The MD5 message digest algorithm as defined in RFC 1321"),
045        SHA_1("SHA-1", 160, "The Secure Hash Algorithm, as defined in Secure Hash Standard, NIST FIPS 180-1"),
046        SHA_256(
047                "SHA-256",
048                256,
049                "New hash algorithms for which the draft Federal Information Processing Standard 180-2, "
050                + "Secure Hash Standard (SHS) is now available.  SHA-256 is a 256-bit hash function intended to provide 128 bits of "
051                + "security against collision attacks."),
052        SHA_384(
053                "SHA-384",
054                384,
055                "New hash algorithms for which the draft Federal Information Processing Standard 180-2, "
056                + "Secure Hash Standard (SHS) is now available.  A 384-bit hash may be obtained by truncating the SHA-512 output."),
057        SHA_512(
058                "SHA-512",
059                512,
060                "New hash algorithms for which the draft Federal Information Processing Standard 180-2, "
061                + "Secure Hash Standard (SHS) is now available.  SHA-512 is a 512-bit hash function intended to provide 256 bits of security.");
062        private final String name;
063        private final String description;
064        private final int numberOfBits;
065        private final int numberOfBytes;
066        private final int numberOfHexChars;
067
068        private Algorithm( String name,
069                           int numberOfBits,
070                           String description ) {
071            assert numberOfBits % 8 == 0;
072            this.name = name;
073            this.description = description;
074            this.numberOfBits = numberOfBits;
075            this.numberOfBytes = this.numberOfBits / 8;
076            this.numberOfHexChars = this.numberOfBits / 4;
077        }
078
079        public String digestName() {
080            return this.name;
081        }
082
083        public String description() {
084            return this.description;
085        }
086
087        /**
088         * Get the length of the hexadecimal representation.
089         * 
090         * @return the number of hexadecimal characters
091         */
092        public int getHexadecimalStringLength() {
093            return numberOfHexChars;
094        }
095
096        /**
097         * Determine whether the supplied string is of the correct format to contain a hexadecimal representation of this
098         * algorithm.
099         * 
100         * @param string the string; may not be null
101         * @return true if the string might contain a hexadecimal representation of this algorithm, or false otherwise
102         */
103        public boolean isHexadecimal( String string ) {
104            return string.length() == getHexadecimalStringLength() && StringUtil.isHexString(string);
105        }
106
107        /**
108         * Get the length of the hexadecimal representation.
109         * 
110         * @return the number of hexadecimal characters
111         */
112        public int getNumberOfBytes() {
113            return numberOfBytes;
114        }
115
116        /**
117         * Get the number of bits that make up a digest.
118         * 
119         * @return the number of bits
120         */
121        public int getNumberOfBits() {
122            return numberOfBits;
123        }
124
125        @Override
126        public String toString() {
127            return digestName();
128        }
129    }
130
131    /**
132     * Get the hash of the supplied content, using the supplied digest algorithm.
133     * 
134     * @param algorithm the hashing function algorithm that should be used
135     * @param content the content to be hashed; may not be null
136     * @return the hash of the contents as a byte array
137     * @throws NoSuchAlgorithmException if the supplied algorithm could not be found
138     * @throws IllegalArgumentException if the algorithm is null
139     */
140    public static byte[] getHash( Algorithm algorithm,
141                                  byte[] content ) throws NoSuchAlgorithmException {
142        CheckArg.isNotNull(algorithm, "algorithm");
143        return getHash(algorithm.digestName(), content);
144    }
145
146    /**
147     * Get the hash of the supplied content, using the supplied digest algorithm.
148     * 
149     * @param algorithm the hashing function algorithm that should be used
150     * @param file the file containing the content to be hashed; may not be null
151     * @return the hash of the contents as a byte array
152     * @throws NoSuchAlgorithmException if the supplied algorithm could not be found
153     * @throws IllegalArgumentException if the algorithm is null
154     * @throws IOException if there is an error reading the file
155     */
156    public static byte[] getHash( Algorithm algorithm,
157                                  File file ) throws NoSuchAlgorithmException, IOException {
158        CheckArg.isNotNull(algorithm, "algorithm");
159        return getHash(algorithm.digestName(), file);
160    }
161
162    /**
163     * Get the hash of the supplied content, using the supplied digest algorithm.
164     * 
165     * @param algorithm the hashing function algorithm that should be used
166     * @param stream the stream containing the content to be hashed; may not be null
167     * @return the hash of the contents as a byte array
168     * @throws NoSuchAlgorithmException if the supplied algorithm could not be found
169     * @throws IllegalArgumentException if the algorithm is null
170     * @throws IOException if there is an error reading the stream
171     */
172    public static byte[] getHash( Algorithm algorithm,
173                                  InputStream stream ) throws NoSuchAlgorithmException, IOException {
174        CheckArg.isNotNull(algorithm, "algorithm");
175        return getHash(algorithm.digestName(), stream);
176    }
177
178    /**
179     * Get the hash of the supplied content, using the digest identified by the supplied name.
180     * 
181     * @param digestName the name of the hashing function (or {@link MessageDigest message digest}) that should be used
182     * @param content the content to be hashed; may not be null
183     * @return the hash of the contents as a byte array
184     * @throws NoSuchAlgorithmException if the supplied algorithm could not be found
185     */
186    public static byte[] getHash( String digestName,
187                                  byte[] content ) throws NoSuchAlgorithmException {
188        MessageDigest digest = MessageDigest.getInstance(digestName);
189        assert digest != null;
190        return digest.digest(content);
191    }
192
193    /**
194     * Get the hash of the supplied content, using the digest identified by the supplied name.
195     * 
196     * @param digestName the name of the hashing function (or {@link MessageDigest message digest}) that should be used
197     * @param file the file whose content is to be hashed; may not be null
198     * @return the hash of the contents as a byte array
199     * @throws NoSuchAlgorithmException if the supplied algorithm could not be found
200     * @throws IOException if there is an error reading the file
201     */
202    public static byte[] getHash( String digestName,
203                                  File file ) throws NoSuchAlgorithmException, IOException {
204        CheckArg.isNotNull(file, "file");
205        MessageDigest digest = MessageDigest.getInstance(digestName);
206        assert digest != null;
207        InputStream in = new BufferedInputStream(new FileInputStream(file));
208        boolean error = false;
209        try {
210            int bufSize = 1024;
211            byte[] buffer = new byte[bufSize];
212            int n = in.read(buffer, 0, bufSize);
213            while (n != -1) {
214                digest.update(buffer, 0, n);
215                n = in.read(buffer, 0, bufSize);
216            }
217        } catch (IOException e) {
218            error = true;
219            throw e;
220        } finally {
221            try {
222                in.close();
223            } catch (IOException e) {
224                if (!error) throw e;
225            }
226        }
227        return digest.digest();
228    }
229
230    /**
231     * Get the hash of the supplied content, using the digest identified by the supplied name. Note that this method never closes
232     * the supplied stream.
233     * 
234     * @param digestName the name of the hashing function (or {@link MessageDigest message digest}) that should be used
235     * @param stream the stream containing the content to be hashed; may not be null
236     * @return the hash of the contents as a byte array
237     * @throws NoSuchAlgorithmException if the supplied algorithm could not be found
238     * @throws IOException if there is an error reading the stream
239     */
240    public static byte[] getHash( String digestName,
241                                  InputStream stream ) throws NoSuchAlgorithmException, IOException {
242        CheckArg.isNotNull(stream, "stream");
243        MessageDigest digest = MessageDigest.getInstance(digestName);
244        assert digest != null;
245        int bufSize = 1024;
246        byte[] buffer = new byte[bufSize];
247        int n = stream.read(buffer, 0, bufSize);
248        while (n != -1) {
249            digest.update(buffer, 0, n);
250            n = stream.read(buffer, 0, bufSize);
251        }
252        return digest.digest();
253    }
254
255    /**
256     * Create an InputStream instance that wraps another stream and that computes the secure hash (using the algorithm with the
257     * supplied name) as the returned stream is used. This can be used to compute the hash of a stream while the stream is being
258     * processed by another reader, and saves from having to process the same stream twice.
259     * 
260     * @param algorithm the hashing function algorithm that should be used
261     * @param inputStream the stream containing the content that is to be hashed
262     * @return the hash of the contents as a byte array
263     * @throws NoSuchAlgorithmException
264     */
265    public static HashingInputStream createHashingStream( Algorithm algorithm,
266                                                          InputStream inputStream ) throws NoSuchAlgorithmException {
267        return createHashingStream(algorithm.digestName(), inputStream);
268    }
269
270    /**
271     * Create an InputStream instance that wraps another stream and that computes the secure hash (using the algorithm with the
272     * supplied name) as the returned stream is used. This can be used to compute the hash of a stream while the stream is being
273     * processed by another reader, and saves from having to process the same stream twice.
274     * 
275     * @param digestName the name of the hashing function (or {@link MessageDigest message digest}) that should be used
276     * @param inputStream the stream containing the content that is to be hashed
277     * @return the hash of the contents as a byte array
278     * @throws NoSuchAlgorithmException
279     */
280    public static HashingInputStream createHashingStream( String digestName,
281                                                          InputStream inputStream ) throws NoSuchAlgorithmException {
282        MessageDigest digest = MessageDigest.getInstance(digestName);
283        return new HashingInputStream(digest, inputStream);
284    }
285
286    /**
287     * Create an Reader instance that wraps another reader and that computes the secure hash (using the algorithm with the
288     * supplied name) as the returned Reader is used. This can be used to compute the hash while the content is being processed,
289     * and saves from having to process the same content twice.
290     * 
291     * @param algorithm the hashing function algorithm that should be used
292     * @param reader the reader containing the content that is to be hashed
293     * @param charset the character set used within the supplied reader; may not be null
294     * @return the hash of the contents as a byte array
295     * @throws NoSuchAlgorithmException
296     */
297    public static HashingReader createHashingReader( Algorithm algorithm,
298                                                     Reader reader,
299                                                     Charset charset ) throws NoSuchAlgorithmException {
300        return createHashingReader(algorithm.digestName(), reader, charset);
301    }
302
303    /**
304     * Create an Reader instance that wraps another reader and that computes the secure hash (using the algorithm with the
305     * supplied name) as the returned Reader is used. This can be used to compute the hash while the content is being processed,
306     * and saves from having to process the same content twice.
307     * 
308     * @param digestName the name of the hashing function (or {@link MessageDigest message digest}) that should be used
309     * @param reader the reader containing the content that is to be hashed
310     * @param charset the character set used within the supplied reader; may not be null
311     * @return the hash of the contents as a byte array
312     * @throws NoSuchAlgorithmException
313     */
314    public static HashingReader createHashingReader( String digestName,
315                                                     Reader reader,
316                                                     Charset charset ) throws NoSuchAlgorithmException {
317        MessageDigest digest = MessageDigest.getInstance(digestName);
318        return new HashingReader(digest, reader, charset);
319    }
320
321    /**
322     * Get the string representation of the supplied binary hash.
323     * 
324     * @param hash the binary hash
325     * @return the hex-encoded representation of the binary hash, or null if the hash is null
326     */
327    public static String asHexString( byte[] hash ) {
328        return hash != null ? StringUtil.getHexString(hash) : null;
329    }
330
331    /**
332     * Computes the sha1 value for the given string.
333     * 
334     * @param string a non-null string
335     * @return the SHA1 value for the given string.
336     */
337    public static String sha1( String string ) {
338        try {
339            byte[] sha1 = SecureHash.getHash(SecureHash.Algorithm.SHA_1, string.getBytes());
340            return SecureHash.asHexString(sha1);
341        } catch (NoSuchAlgorithmException e) {
342            throw new SystemFailureException(e);
343        }
344    }
345
346    public static class HashingInputStream extends InputStream {
347        private final MessageDigest digest;
348        private final InputStream stream;
349        private byte[] hash;
350
351        protected HashingInputStream( MessageDigest digest,
352                                      InputStream input ) {
353            this.digest = digest;
354            this.stream = input;
355        }
356
357        @Override
358        public int read() throws IOException {
359            int result = stream.read();
360            if (result != -1) {
361                digest.update((byte)result);
362            }
363            return result;
364        }
365
366        @Override
367        public int read( byte[] b,
368                         int off,
369                         int len ) throws IOException {
370            // Read from the stream ...
371            int n = stream.read(b, off, len);
372            if (n != -1) {
373                digest.update(b, off, n);
374            }
375            return n;
376        }
377
378        @Override
379        public int read( byte[] b ) throws IOException {
380            int n = stream.read(b);
381            if (n != -1) {
382                digest.update(b, 0, n);
383            }
384            return n;
385        }
386
387        @Override
388        public void close() throws IOException {
389            stream.close();
390            if (hash == null) hash = digest.digest();
391        }
392
393        /**
394         * Get the hash of the content read by this stream. This method will return null if the stream has not yet been closed.
395         * 
396         * @return the hash of the contents as a byte array, or null if the stream has not yet been closed
397         */
398        public byte[] getHash() {
399            return hash;
400        }
401
402        /**
403         * Get the string representation of the binary hash of the content read by this stream. This method will return null if
404         * the stream has not yet been closed.
405         * 
406         * @return the hex-encoded representation of the binary hash of the contents, or null if the stream has not yet been
407         *         closed
408         */
409        public String getHashAsHexString() {
410            return SecureHash.asHexString(hash);
411        }
412    }
413
414    public static class HashingReader extends Reader {
415        private final MessageDigest digest;
416        private final Reader stream;
417        private byte[] hash;
418        private final CharsetEncoder encoder;
419
420        protected HashingReader( MessageDigest digest,
421                                 Reader input,
422                                 Charset charset ) {
423            this.digest = digest;
424            this.stream = input;
425            this.encoder = charset.newEncoder();
426        }
427
428        /**
429         * {@inheritDoc}
430         * 
431         * @see java.io.Reader#read()
432         */
433        @Override
434        public int read() throws IOException {
435            int result = stream.read();
436            if (result != -1) {
437                digest.update((byte)result);
438            }
439            return result;
440        }
441
442        /**
443         * {@inheritDoc}
444         * 
445         * @see java.io.Reader#read(char[], int, int)
446         */
447        @Override
448        public int read( char[] b,
449                         int off,
450                         int len ) throws IOException {
451            // Read from the stream ...
452            int n = stream.read(b, off, len);
453            if (n != -1) {
454                byte[] bytes = encoder.encode(CharBuffer.wrap(b)).array();
455                digest.update(bytes, off, n);
456            }
457            return n;
458        }
459
460        /**
461         * {@inheritDoc}
462         * 
463         * @see java.io.Reader#read(char[])
464         */
465        @Override
466        public int read( char[] b ) throws IOException {
467            int n = stream.read(b);
468            if (n != -1) {
469                byte[] bytes = encoder.encode(CharBuffer.wrap(b)).array();
470                digest.update(bytes, 0, n);
471            }
472            return n;
473        }
474
475        /**
476         * {@inheritDoc}
477         * 
478         * @see java.io.InputStream#close()
479         */
480        @Override
481        public void close() throws IOException {
482            stream.close();
483            if (hash == null) hash = digest.digest();
484        }
485
486        /**
487         * Get the hash of the content read by this reader. This method will return null if the reader has not yet been closed.
488         * 
489         * @return the hash of the contents as a byte array, or null if the reader has not yet been closed
490         */
491        public byte[] getHash() {
492            return hash;
493        }
494
495        /**
496         * Get the string representation of the binary hash of the content read by this reader. This method will return null if
497         * the reader has not yet been closed.
498         * 
499         * @return the hex-encoded representation of the binary hash of the contents, or null if the reader has not yet been
500         *         closed
501         */
502        public String getHashAsHexString() {
503            return SecureHash.asHexString(hash);
504        }
505    }
506
507    private SecureHash() {
508    }
509}