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 */
019package org.apache.wiki.util;
020
021import java.nio.charset.StandardCharsets;
022import java.security.MessageDigest;
023import java.security.NoSuchAlgorithmException;
024import java.security.SecureRandom;
025import java.util.Arrays;
026import java.util.Base64;
027import java.util.Random;
028
029
030/**
031 * Hashes and verifies salted SHA-1 passwords, which are compliant with RFC
032 * 2307.
033 */
034public final class CryptoUtil
035{
036    private static final String SSHA = "{SSHA}";
037
038    private static final String SHA1 = "{SHA-1}";
039
040    private static final String SHA256 = "{SHA-256}";
041
042    private static final Random RANDOM = new SecureRandom();
043
044    private static final int DEFAULT_SALT_SIZE = 8;
045
046    private static final Object HELP = "--help";
047
048    private static final Object HASH = "--hash";
049
050    private static final Object VERIFY = "--verify";
051
052    /**
053     * Private constructor to prevent direct instantiation.
054     */
055    private CryptoUtil()
056    {
057    }
058
059    /**
060     * <p>
061     * Convenience method for hashing and verifying salted SHA-1 or SHA-256 passwords from
062     * the command line. This method requires <code>commons-codec-1.3.jar</code>
063     * (or a newer version) to be on the classpath. Command line arguments are
064     * as follows:
065     * </p>
066     * <ul>
067     * <li><code>--hash <var>password</var> SSHA</code> - hashes <var>password</var></code>
068     * and prints a password digest that looks like this: <blockquote><code>{SSHA}yfT8SRT/WoOuNuA6KbJeF10OznZmb28=</code></blockquote></li>
069     * <li><code>--verify <var>password</var> <var>digest</var></code> -
070     * verifies <var>password</var> by extracting the salt from <var>digest</var>
071     * (which is identical to what is printed by <code>--hash</code>) and
072     * re-computing the digest again using the password and salt. If the
073     * password supplied is the same as the one used to create the original
074     * digest, <code>true</code> will be printed; otherwise <code>false</code></li>
075     * </ul>
076     * <p>For example, one way to use this utility is to change to JSPWiki's <code>build</code> directory
077     * and type the following command:</p>
078     * <blockquote><code>java -cp JSPWiki.jar:../lib/commons-codec-1.3.jar org.apache.wiki.util.CryptoUtil --hash mynewpassword</code></blockquote>
079     * 
080     * @param args arguments for this method as described above
081     * @throws Exception Catches nothing; throws everything up.
082     */
083    public static void main( final String[] args ) throws Exception
084    {
085        // Print help if the user requested it, or if no arguments
086        if( args.length == 0 || (args.length == 1 && HELP.equals( args[0] )) )
087        {
088            System.out.println( "Usage: CryptoUtil [options] " );
089            System.out.println( "   --hash   password algorithm             create hash for password" );
090            System.out.println( "   --verify password digest algorithm      verify password for digest" );
091            System.out.println( "Valid algorithm options are {SSHA} and {SHA-256}. If no algorithm is specified or an unsupported algorithm is specified, SHA-256 is used." );
092        }
093
094        // User wants to hash the password
095        if( HASH.equals( args[0] ) )
096        {
097            if( args.length < 2 )
098            {
099                throw new IllegalArgumentException( "Error: --hash requires a 'password' argument." );
100            }
101            final String password = args[1].trim();
102            final String algorithm = args.length > 2 ? args[2].trim() : SHA256;
103
104            System.out.println( CryptoUtil.getSaltedPassword( password.getBytes( StandardCharsets.UTF_8 ), algorithm ) );
105        }
106
107        // User wants to verify an existing password
108        else if( VERIFY.equals( args[0] ) )
109        {
110            if( args.length < 3 )
111            {
112                throw new IllegalArgumentException( "Error: --hash requires 'password' and 'digest' arguments." );
113            }
114            final String password = args[1].trim();
115            final String digest = args[2].trim();
116
117            System.out.println( CryptoUtil.verifySaltedPassword( password.getBytes( StandardCharsets.UTF_8 ), digest ) );
118        }
119
120        else
121        {
122            System.out.println( "Wrong usage. Try --help." );
123        }
124    }
125
126    /**
127     * <p>
128     * Creates an RFC 2307-compliant salted, hashed password with the SHA1 or SHA-256
129     * MessageDigest algorithm. After the password is digested, the first 20 or 32
130     * bytes of the digest will be the actual password hash; the remaining bytes
131     * will be a randomly generated salt of length {@link #DEFAULT_SALT_SIZE},
132     * for example: <blockquote><code>{SSHA}3cGWem65NCEkF5Ew5AEk45ak8LHUWAwPVXAyyw==</code></blockquote>
133     * </p>
134     * <p>
135     * In layman's terms, the formula is
136     * <code>digest( secret + salt ) + salt</code>. The resulting digest is
137     * Base64-encoded.
138     * </p>
139     * <p>
140     * Note that successive invocations of this method with the same password
141     * will result in different hashes! (This, of course, is exactly the point.)
142     * </p>
143     * 
144     * @param password the password to be digested
145     * @return the Base64-encoded password hash, prepended by
146     *         <code>{SSHA}</code> or <code>{SHA256}</code>.
147     * @throws NoSuchAlgorithmException If your JVM does not supply the necessary algorithm. Should not happen.
148     */
149    public static String getSaltedPassword(final byte[] password, final String algorithm ) throws NoSuchAlgorithmException
150    {
151        final byte[] salt = new byte[DEFAULT_SALT_SIZE];
152        RANDOM.nextBytes( salt );
153
154        return getSaltedPassword( password, salt, algorithm );
155    }
156
157    /**
158     * <p>
159     * Helper method that creates an RFC 2307-compliant salted, hashed password with the SHA1 or SHA256
160     * MessageDigest algorithm. After the password is digested, the first 20 or 32
161     * bytes of the digest will be the actual password hash; the remaining bytes
162     * will be the salt. Thus, supplying a password <code>testing123</code>
163     * and a random salt <code>foo</code> produces the hash when using SHA1:
164     * </p>
165     * <blockquote><code>{SSHA}yfT8SRT/WoOuNuA6KbJeF10OznZmb28=</code></blockquote>
166     * <p>
167     * In layman's terms, the formula is
168     * <code>digest( secret + salt ) + salt</code>. The resulting digest is Base64-encoded.</p>
169     * 
170     * @param password the password to be digested
171     * @param salt the random salt
172     * @return the Base64-encoded password hash, prepended by <code>{SSHA}</code> or <code>{SHA256}</code>.
173     * @throws NoSuchAlgorithmException If your JVM does not supply the necessary algorithm. Should not happen.
174     */
175    static String getSaltedPassword(final byte[] password, final byte[] salt, final String algorithm ) throws NoSuchAlgorithmException
176    {
177        //The term SSHA is used as a password prefix for backwards compatibility, but we use SHA-1 when fetching an instance
178        //of MessageDigest, as it is the guaranteed option. We also need to remove curly braces surrounding the string for
179        //backwards compatibility.
180        String algorithmToUse = algorithm.equals(SSHA) ? SHA1 : algorithm;
181        final MessageDigest digest = MessageDigest.getInstance( algorithmToUse.substring( 1, algorithmToUse.length() -1 ) );
182        digest.update( password );
183        final byte[] hash = digest.digest( salt );
184
185        // Create an array with the hash plus the salt
186        final byte[] all = new byte[hash.length + salt.length];
187        System.arraycopy(hash, 0, all, 0, hash.length);
188        System.arraycopy(salt, 0, all, hash.length + 0, salt.length);
189        final byte[] base64 = Base64.getEncoder().encode( all );
190        
191        return algorithm + new String( base64, StandardCharsets.UTF_8 );
192    }
193
194    /**
195     *  Compares a password to a given entry and returns true, if it matches.
196     *
197     *  @param password The password in bytes.
198     *  @param entry The password entry, typically starting with {SSHA}.
199     *  @return True, if the password matches.
200     *  @throws NoSuchAlgorithmException If there is no SHA available.
201     */
202    public static boolean verifySaltedPassword(final byte[] password, final String entry ) throws NoSuchAlgorithmException
203    {
204        if( !entry.startsWith( SSHA ) && !entry.startsWith( SHA256 ) )
205        {
206            throw new IllegalArgumentException( "Hash not prefixed by expected algorithm; is it really a salted hash?" );
207        }
208        String algorithm = entry.startsWith( SSHA ) ? SSHA : SHA256;
209
210        final byte[] challenge = Base64.getDecoder().decode( entry.substring( algorithm.length() )
211                .getBytes( StandardCharsets.UTF_8 ) );
212
213        // Extract the password hash and salt
214        final byte[] passwordHash = extractPasswordHash( challenge, algorithm.equals(SSHA) ? 20 : 32 );
215        final byte[] salt = extractSalt( challenge, algorithm.equals(SSHA) ? 20 : 32  );
216
217        // Re-create the hash using the password and the extracted salt
218        // The term SSHA is used as a password prefix for backwards compatibility, but we use SHA-1 when fetching an instance
219        // of MessageDigest, as it is the guaranteed option. We also need to remove curly braces surrounding the string for
220        // backwards compatibility.
221        String algorithmToUse = algorithm.equals(SSHA) ? SHA1 : algorithm;
222        final MessageDigest digest = MessageDigest.getInstance( algorithmToUse.substring( 1, algorithmToUse.length() -1 ) );
223        digest.update( password );
224        final byte[] hash = digest.digest( salt );
225
226        // See if our extracted hash matches what we just re-created
227        return Arrays.equals( passwordHash, hash );
228    }
229
230    /**
231     * Helper method that extracts the hashed password fragment from a supplied salted SHA-1 or SHA-256 digest
232     * by taking all of the characters before position 20 or 32 depending on algorithm.
233     * 
234     * @param digest the salted digest, which is assumed to have been
235     *            previously decoded from Base64.
236     * @return the password hash
237     * @throws IllegalArgumentException if the length of the supplied digest is
238     *             less than or equal to 20 bytes
239     */
240    static byte[] extractPasswordHash(final byte[] digest, final int hashLength ) throws IllegalArgumentException
241    {
242        if( digest.length < hashLength )
243        {
244            throw new IllegalArgumentException( "Hash was shorter than expected; could not extract password hash!" );
245        }
246
247        // Extract the password hash
248        final byte[] hash = new byte[hashLength];
249        System.arraycopy(digest, 0, hash, 0, hashLength);
250
251        return hash;
252    }
253
254    /**
255     * Helper method that extracts the salt from supplied salted digest by taking all of the
256     * characters after a given index.
257     * 
258     * @param digest the salted digest, which is assumed to have been previously
259     *            decoded from Base64.
260     * @return the salt
261     * @throws IllegalArgumentException if the length of the supplied digest is
262     *             less than given length.
263     */
264    static byte[] extractSalt(final byte[] digest, final int hashLength ) throws IllegalArgumentException
265    {
266        if( digest.length <= hashLength )
267        {
268            throw new IllegalArgumentException( "Hash was shorter than expected; we found no salt!" );
269        }
270
271        // Extract the salt
272        final byte[] salt = new byte[digest.length - hashLength];
273        System.arraycopy(digest, hashLength, salt, 0, digest.length - hashLength);
274
275        return salt;
276    }
277}