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}