001    /* 
002        Licensed to the Apache Software Foundation (ASF) under one
003        or more contributor license agreements.  See the NOTICE file
004        distributed with this work for additional information
005        regarding copyright ownership.  The ASF licenses this file
006        to you under the Apache License, Version 2.0 (the
007        "License"); you may not use this file except in compliance
008        with the License.  You may obtain a copy of the License at
009    
010           http://www.apache.org/licenses/LICENSE-2.0
011    
012        Unless required by applicable law or agreed to in writing,
013        software distributed under the License is distributed on an
014        "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
015        KIND, either express or implied.  See the License for the
016        specific language governing permissions and limitations
017        under the License.  
018     */
019    package org.apache.wiki.util;
020    
021    import java.io.UnsupportedEncodingException;
022    import java.security.MessageDigest;
023    import java.security.NoSuchAlgorithmException;
024    import java.security.SecureRandom;
025    import java.util.Arrays;
026    import java.util.Random;
027    
028    import org.apache.commons.codec.binary.Base64;
029    import org.apache.log4j.Logger;
030    
031    /**
032     * Hashes and verifies salted SHA-1 passwords, which are compliant with RFC
033     * 2307.
034     */
035    public final class CryptoUtil
036    {
037        private static final Logger log = Logger.getLogger( CryptoUtil.class );
038    
039        private static final String SSHA = "{SSHA}";
040    
041        private static final Random RANDOM = new SecureRandom();
042    
043        private static final int DEFAULT_SALT_SIZE = 8;
044    
045        private static final Object HELP = "--help";
046    
047        private static final Object HASH = "--hash";
048    
049        private static final Object VERIFY = "--verify";
050    
051        /**
052         * Private constructor to prevent direct instantiation.
053         */
054        private CryptoUtil()
055        {
056        }
057    
058        /**
059         * <p>
060         * Convenience method for hashing and verifying salted SHA-1 passwords from
061         * the command line. This method requires <code>commons-codec-1.3.jar</code>
062         * (or a newer version) to be on the classpath. Command line arguments are
063         * as follows:
064         * </p>
065         * <ul>
066         * <li><code>--hash <var>password</var></code> - hashes <var>password</var></code>
067         * and prints a password digest that looks like this: <blockquote><code>{SSHA}yfT8SRT/WoOuNuA6KbJeF10OznZmb28=</code></blockquote></li>
068         * <li><code>--verify <var>password</var> <var>digest</var></code> -
069         * verifies <var>password</var> by extracting the salt from <var>digest</var>
070         * (which is identical to what is printed by <code>--hash</code>) and
071         * re-computing the digest again using the password and salt. If the
072         * password supplied is the same as the one used to create the original
073         * digest, <code>true</code> will be printed; otherwise <code>false</code></li>
074         * </ul>
075         * <p>For example, one way to use this utility is to change to JSPWiki's <code>build</code> directory
076         * and type the following command:</p>
077         * <blockquote><code>java -cp JSPWiki.jar:../lib/commons-codec-1.3.jar org.apache.wiki.util.CryptoUtil --hash mynewpassword</code></blockquote>
078         * 
079         * @param args arguments for this method as described above
080         * @throws Exception Catches nothing; throws everything up.
081         */
082        public static void main( final String[] args ) throws Exception
083        {
084            // Print help if the user requested it, or if no arguments
085            if( args.length == 0 || (args.length == 1 && HELP.equals( args[0] )) )
086            {
087                System.out.println( "Usage: CryptUtil [options] " );
088                System.out.println( "   --hash   password             create hash for password" );
089                System.out.println( "   --verify password digest      verify password for digest" );
090                System.exit( 0 );
091            }
092    
093            // User wants to hash the password
094            if( HASH.equals( args[0] ) )
095            {
096                if( args.length < 2 )
097                {
098                    throw new IllegalArgumentException( "Error: --hash requires a 'password' argument." );
099                }
100                final String password = args[1].trim();
101                System.out.println( CryptoUtil.getSaltedPassword( password.getBytes("UTF-8") ) );
102            }
103    
104            // User wants to verify an existing password
105            else if( VERIFY.equals( args[0] ) )
106            {
107                if( args.length < 3 )
108                {
109                    throw new IllegalArgumentException( "Error: --hash requires 'password' and 'digest' arguments." );
110                }
111                final String password = args[1].trim();
112                final String digest = args[2].trim();
113                System.out.println( CryptoUtil.verifySaltedPassword( password.getBytes("UTF-8"), digest ) );
114            }
115    
116            else
117            {
118                System.out.println( "Wrong usage. Try --help." );
119            }
120        }
121    
122        /**
123         * <p>
124         * Creates an RFC 2307-compliant salted, hashed password with the SHA1
125         * MessageDigest algorithm. After the password is digested, the first 20
126         * bytes of the digest will be the actual password hash; the remaining bytes
127         * will be a randomly generated salt of length {@link #DEFAULT_SALT_SIZE},
128         * for example: <blockquote><code>{SSHA}3cGWem65NCEkF5Ew5AEk45ak8LHUWAwPVXAyyw==</code></blockquote>
129         * </p>
130         * <p>
131         * In layman's terms, the formula is
132         * <code>digest( secret + salt ) + salt</code>. The resulting digest is
133         * Base64-encoded.
134         * </p>
135         * <p>
136         * Note that successive invocations of this method with the same password
137         * will result in different hashes! (This, of course, is exactly the point.)
138         * </p>
139         * 
140         * @param password the password to be digested
141         * @return the Base64-encoded password hash, prepended by
142         *         <code>{SSHA}</code>.
143         * @throws NoSuchAlgorithmException If your JVM is completely b0rked and does not have SHA.
144         */
145        public static String getSaltedPassword( byte[] password ) throws NoSuchAlgorithmException
146        {
147            byte[] salt = new byte[DEFAULT_SALT_SIZE];
148            RANDOM.nextBytes( salt );
149            return getSaltedPassword( password, salt );
150        }
151    
152        /**
153         * <p>
154         * Helper method that creates an RFC 2307-compliant salted, hashed password with the SHA1
155         * MessageDigest algorithm. After the password is digested, the first 20
156         * bytes of the digest will be the actual password hash; the remaining bytes
157         * will be the salt. Thus, supplying a password <code>testing123</code>
158         * and a random salt <code>foo</code> produces the hash:
159         * </p>
160         * <blockquote><code>{SSHA}yfT8SRT/WoOuNuA6KbJeF10OznZmb28=</code></blockquote>
161         * <p>
162         * In layman's terms, the formula is
163         * <code>digest( secret + salt ) + salt</code>. The resulting digest is Base64-encoded.</p>
164         * 
165         * @param password the password to be digested
166         * @param salt the random salt
167         * @return the Base64-encoded password hash, prepended by <code>{SSHA}</code>.
168         * @throws NoSuchAlgorithmException If your JVM is totally b0rked and does not have SHA1.
169         */
170        protected static String getSaltedPassword( byte[] password, byte[] salt ) throws NoSuchAlgorithmException
171        {
172            MessageDigest digest = MessageDigest.getInstance( "SHA" );
173            digest.update( password );
174            byte[] hash = digest.digest( salt );
175    
176            // Create an array with the hash plus the salt
177            byte[] all = new byte[hash.length + salt.length];
178            for( int i = 0; i < hash.length; i++ )
179            {
180                all[i] = hash[i];
181            }
182            for( int i = 0; i < salt.length; i++ )
183            {
184                all[hash.length + i] = salt[i];
185            }
186            byte[] base64 = Base64.encodeBase64( all );
187            
188            String saltedString = null;
189            try
190            {
191                saltedString = SSHA + new String( base64, "UTF8" );
192            }
193            catch( UnsupportedEncodingException e )
194            {
195                log.fatal( "You do not have UTF-8!?!" );
196            }
197            return saltedString;
198        }
199    
200        /**
201         *  Compares a password to a given entry and returns true, if it matches.
202         *  
203         *  @param password The password in bytes.
204         *  @param entry The password entry, typically starting with {SSHA}.
205         *  @return True, if the password matches.
206         *  @throws NoSuchAlgorithmException If there is no SHA available.
207         *  @throws UnsupportedEncodingException If no UTF-8 encoding is available 
208         */
209        public static boolean verifySaltedPassword( byte[] password, String entry ) 
210            throws NoSuchAlgorithmException, UnsupportedEncodingException
211        {
212            // First, extract everything after {SSHA} and decode from Base64
213            if( !entry.startsWith( SSHA ) )
214            {
215                throw new IllegalArgumentException( "Hash not prefixed by {SSHA}; is it really a salted hash?" );
216            }
217            byte[] challenge = Base64.decodeBase64( entry.substring( 6 ).getBytes("UTF-8") );
218    
219            // Extract the password hash and salt
220            byte[] passwordHash = extractPasswordHash( challenge );
221            byte[] salt = extractSalt( challenge );
222    
223            // Re-create the hash using the password and the extracted salt
224            MessageDigest digest = MessageDigest.getInstance( "SHA" );
225            digest.update( password );
226            byte[] hash = digest.digest( salt );
227    
228            // See if our extracted hash matches what we just re-created
229            return Arrays.equals( passwordHash, hash );
230        }
231    
232        /**
233         * Helper method that extracts the hashed password fragment from a supplied salted SHA digest
234         * by taking all of the characters before position 20.
235         * 
236         * @param digest the salted digest, which is assumed to have been
237         *            previously decoded from Base64.
238         * @return the password hash
239         * @throws IllegalArgumentException if the length of the supplied digest is
240         *             less than or equal to 20 bytes
241         */
242        protected static byte[] extractPasswordHash( byte[] digest ) throws IllegalArgumentException
243        {
244            if( digest.length < 20 )
245            {
246                throw new IllegalArgumentException( "Hash was less than 20 characters; could not extract password hash!" );
247            }
248    
249            // Extract the password hash
250            byte[] hash = new byte[20];
251            for( int i = 0; i < 20; i++ )
252            {
253                hash[i] = digest[i];
254            }
255    
256            return hash;
257        }
258    
259        /**
260         * Helper method that extracts the salt from supplied salted digest by taking all of the
261         * characters at position 20 and higher.
262         * 
263         * @param digest the salted digest, which is assumed to have been previously
264         *            decoded from Base64.
265         * @return the salt
266         * @throws IllegalArgumentException if the length of the supplied digest is
267         *             less than or equal to 20 bytes
268         */
269        protected static byte[] extractSalt( byte[] digest ) throws IllegalArgumentException
270        {
271            if( digest.length <= 20 )
272            {
273                throw new IllegalArgumentException( "Hash was less than 21 characters; we found no salt!" );
274            }
275    
276            // Extract the salt
277            byte[] salt = new byte[digest.length - 20];
278            for( int i = 20; i < digest.length; i++ )
279            {
280                salt[i - 20] = digest[i];
281            }
282    
283            return salt;
284        }
285    }