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