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}