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