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}