    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}