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