View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one
3    * or more contributor license agreements.  See the NOTICE file
4    * distributed with this work for additional information
5    * regarding copyright ownership.  The ASF licenses this file
6    * to you under the Apache License, Version 2.0 (the
7    * "License"); you may not use this file except in compliance
8    * with the License.  You may obtain a copy of the License at
9    *
10   *     http://www.apache.org/licenses/LICENSE-2.0
11   *
12   * Unless required by applicable law or agreed to in writing,
13   * software distributed under the License is distributed on an
14   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15   * KIND, either express or implied.  See the License for the
16   * specific language governing permissions and limitations
17   * under the License.
18   */
19  package org.apache.shiro.tools.hasher;
20  
21  import org.apache.commons.cli.CommandLine;
22  import org.apache.commons.cli.CommandLineParser;
23  import org.apache.commons.cli.HelpFormatter;
24  import org.apache.commons.cli.Option;
25  import org.apache.commons.cli.Options;
26  import org.apache.commons.cli.DefaultParser;
27  import org.apache.shiro.authc.credential.DefaultPasswordService;
28  import org.apache.shiro.codec.Base64;
29  import org.apache.shiro.codec.Hex;
30  import org.apache.shiro.crypto.SecureRandomNumberGenerator;
31  import org.apache.shiro.crypto.UnknownAlgorithmException;
32  import org.apache.shiro.crypto.hash.DefaultHashService;
33  import org.apache.shiro.crypto.hash.Hash;
34  import org.apache.shiro.crypto.hash.HashRequest;
35  import org.apache.shiro.crypto.hash.SimpleHashRequest;
36  import org.apache.shiro.crypto.hash.format.DefaultHashFormatFactory;
37  import org.apache.shiro.crypto.hash.format.HashFormat;
38  import org.apache.shiro.crypto.hash.format.HashFormatFactory;
39  import org.apache.shiro.crypto.hash.format.HexFormat;
40  import org.apache.shiro.crypto.hash.format.Shiro1CryptFormat;
41  import org.apache.shiro.io.ResourceUtils;
42  import org.apache.shiro.util.ByteSource;
43  import org.apache.shiro.util.StringUtils;
44  
45  import java.io.File;
46  import java.io.IOException;
47  import java.util.Arrays;
48  
49  /**
50   * Commandline line utility to hash data such as strings, passwords, resources (files, urls, etc).
51   * <p/>
52   * Usage:
53   * <pre>
54   * java -jar shiro-tools-hasher<em>-version</em>-cli.jar
55   * </pre>
56   * This will print out all supported options with documentation.
57   *
58   * @since 1.2
59   */
60  public final class Hasher {
61  
62      private static final String HEX_PREFIX = "0x";
63      private static final String DEFAULT_ALGORITHM_NAME = "MD5";
64      private static final String DEFAULT_PASSWORD_ALGORITHM_NAME = DefaultPasswordService.DEFAULT_HASH_ALGORITHM;
65      private static final int DEFAULT_GENERATED_SALT_SIZE = 128;
66      private static final int DEFAULT_NUM_ITERATIONS = 1;
67      private static final int DEFAULT_PASSWORD_NUM_ITERATIONS = DefaultPasswordService.DEFAULT_HASH_ITERATIONS;
68  
69      private static final Option ALGORITHM = new Option("a", "algorithm", true, "hash algorithm name.  Defaults to SHA-256 when password hashing, MD5 otherwise.");
70      private static final Option DEBUG = new Option("d", "debug", false, "show additional error (stack trace) information.");
71      private static final Option FORMAT = new Option("f", "format", true, "hash output format.  Defaults to 'shiro1' when password hashing, 'hex' otherwise.  See below for more information.");
72      private static final Option HELP = new Option("help", "help", false, "show this help message.");
73      private static final Option ITERATIONS = new Option("i", "iterations", true, "number of hash iterations.  Defaults to " + DEFAULT_PASSWORD_NUM_ITERATIONS + " when password hashing, 1 otherwise.");
74      private static final Option PASSWORD = new Option("p", "password", false, "hash a password (disable typing echo)");
75      private static final Option PASSWORD_NC = new Option("pnc", "pnoconfirm", false, "hash a password (disable typing echo) but disable password confirmation prompt.");
76      private static final Option RESOURCE = new Option("r", "resource", false, "read and hash the resource located at <value>.  See below for more information.");
77      private static final Option SALT = new Option("s", "salt", true, "use the specified salt.  <arg> is plaintext.");
78      private static final Option SALT_BYTES = new Option("sb", "saltbytes", true, "use the specified salt bytes.  <arg> is hex or base64 encoded text.");
79      private static final Option SALT_GEN = new Option("gs", "gensalt", false, "generate and use a random salt. Defaults to true when password hashing, false otherwise.");
80      private static final Option NO_SALT_GEN = new Option("ngs", "nogensalt", false, "do NOT generate and use a random salt (valid during password hashing).");
81      private static final Option SALT_GEN_SIZE = new Option("gss", "gensaltsize", true, "the number of salt bits (not bytes!) to generate.  Defaults to 128.");
82      private static final Option PRIVATE_SALT = new Option("ps", "privatesalt", true, "use the specified private salt.  <arg> is plaintext.");
83      private static final Option PRIVATE_SALT_BYTES = new Option("psb", "privatesaltbytes", true, "use the specified private salt bytes.  <arg> is hex or base64 encoded text.");
84  
85      private static final String SALT_MUTEX_MSG = createMutexMessage(SALT, SALT_BYTES);
86  
87      private static final HashFormatFactory HASH_FORMAT_FACTORY = new DefaultHashFormatFactory();
88  
89      static {
90          ALGORITHM.setArgName("name");
91          SALT_GEN_SIZE.setArgName("numBits");
92          ITERATIONS.setArgName("num");
93          SALT.setArgName("sval");
94          SALT_BYTES.setArgName("encTxt");
95      }
96  
97      public static void main(String[] args) {
98  
99          CommandLineParser parser = new DefaultParser();
100 
101         Options options = new Options();
102         options.addOption(HELP).addOption(DEBUG).addOption(ALGORITHM).addOption(ITERATIONS);
103         options.addOption(RESOURCE).addOption(PASSWORD).addOption(PASSWORD_NC);
104         options.addOption(SALT).addOption(SALT_BYTES).addOption(SALT_GEN).addOption(SALT_GEN_SIZE).addOption(NO_SALT_GEN);
105         options.addOption(PRIVATE_SALT).addOption(PRIVATE_SALT_BYTES);
106         options.addOption(FORMAT);
107 
108         boolean debug = false;
109         String algorithm = null; //user unspecified
110         int iterations = 0; //0 means unspecified by the end-user
111         boolean resource = false;
112         boolean password = false;
113         boolean passwordConfirm = true;
114         String saltString = null;
115         String saltBytesString = null;
116         boolean generateSalt = false;
117         int generatedSaltSize = DEFAULT_GENERATED_SALT_SIZE;
118         String privateSaltString = null;
119         String privateSaltBytesString = null;
120 
121         String formatString = null;
122 
123         char[] passwordChars = null;
124 
125         try {
126             CommandLine line = parser.parse(options, args);
127 
128             if (line.hasOption(HELP.getOpt())) {
129                 printHelpAndExit(options, null, debug, 0);
130             }
131             if (line.hasOption(DEBUG.getOpt())) {
132                 debug = true;
133             }
134             if (line.hasOption(ALGORITHM.getOpt())) {
135                 algorithm = line.getOptionValue(ALGORITHM.getOpt());
136             }
137             if (line.hasOption(ITERATIONS.getOpt())) {
138                 iterations = getRequiredPositiveInt(line, ITERATIONS);
139             }
140             if (line.hasOption(PASSWORD.getOpt())) {
141                 password = true;
142                 generateSalt = true;
143             }
144             if (line.hasOption(RESOURCE.getOpt())) {
145                 resource = true;
146             }
147             if (line.hasOption(PASSWORD_NC.getOpt())) {
148                 password = true;
149                 generateSalt = true;
150                 passwordConfirm = false;
151             }
152             if (line.hasOption(SALT.getOpt())) {
153                 saltString = line.getOptionValue(SALT.getOpt());
154             }
155             if (line.hasOption(SALT_BYTES.getOpt())) {
156                 saltBytesString = line.getOptionValue(SALT_BYTES.getOpt());
157             }
158             if (line.hasOption(NO_SALT_GEN.getOpt())) {
159                 generateSalt = false;
160             }
161             if (line.hasOption(SALT_GEN.getOpt())) {
162                 generateSalt = true;
163             }
164             if (line.hasOption(SALT_GEN_SIZE.getOpt())) {
165                 generateSalt = true;
166                 generatedSaltSize = getRequiredPositiveInt(line, SALT_GEN_SIZE);
167                 if (generatedSaltSize % 8 != 0) {
168                     throw new IllegalArgumentException("Generated salt size must be a multiple of 8 (e.g. 128, 192, 256, 512, etc).");
169                 }
170             }
171             if (line.hasOption(PRIVATE_SALT.getOpt())) {
172                 privateSaltString = line.getOptionValue(PRIVATE_SALT.getOpt());
173             }
174             if (line.hasOption(PRIVATE_SALT_BYTES.getOpt())) {
175                 privateSaltBytesString = line.getOptionValue(PRIVATE_SALT_BYTES.getOpt());
176             }
177             if (line.hasOption(FORMAT.getOpt())) {
178                 formatString = line.getOptionValue(FORMAT.getOpt());
179             }
180 
181             String sourceValue;
182 
183             Object source;
184 
185             if (password) {
186                 passwordChars = readPassword(passwordConfirm);
187                 source = passwordChars;
188             } else {
189                 String[] remainingArgs = line.getArgs();
190                 if (remainingArgs == null || remainingArgs.length != 1) {
191                     printHelpAndExit(options, null, debug, -1);
192                 }
193 
194                 assert remainingArgs != null;
195                 sourceValue = toString(remainingArgs);
196 
197                 if (resource) {
198                     if (!ResourceUtils.hasResourcePrefix(sourceValue)) {
199                         source = toFile(sourceValue);
200                     } else {
201                         source = ResourceUtils.getInputStreamForPath(sourceValue);
202                     }
203                 } else {
204                     source = sourceValue;
205                 }
206             }
207 
208             if (algorithm == null) {
209                 if (password) {
210                     algorithm = DEFAULT_PASSWORD_ALGORITHM_NAME;
211                 } else {
212                     algorithm = DEFAULT_ALGORITHM_NAME;
213                 }
214             }
215 
216             if (iterations < DEFAULT_NUM_ITERATIONS) {
217                 //Iterations were not specified.  Default to 350,000 when password hashing, and 1 for everything else:
218                 if (password) {
219                     iterations = DEFAULT_PASSWORD_NUM_ITERATIONS;
220                 } else {
221                     iterations = DEFAULT_NUM_ITERATIONS;
222                 }
223             }
224 
225             ByteSource publicSalt = getSalt(saltString, saltBytesString, generateSalt, generatedSaltSize);
226             ByteSource privateSalt = getSalt(privateSaltString, privateSaltBytesString, false, generatedSaltSize);
227             HashRequest hashRequest = new SimpleHashRequest(algorithm, ByteSource.Util.bytes(source), publicSalt, iterations);
228 
229             DefaultHashServicervice.html#DefaultHashService">DefaultHashService hashService = new DefaultHashService();
230             hashService.setPrivateSalt(privateSalt);
231             Hash hash = hashService.computeHash(hashRequest);
232 
233             if (formatString == null) {
234                 //Output format was not specified.  Default to 'shiro1' when password hashing, and 'hex' for
235                 //everything else:
236                 if (password) {
237                     formatString = Shiro1CryptFormat.class.getName();
238                 } else {
239                     formatString = HexFormat.class.getName();
240                 }
241             }
242 
243             HashFormat format = HASH_FORMAT_FACTORY.getInstance(formatString);
244 
245             if (format == null) {
246                 throw new IllegalArgumentException("Unrecognized hash format '" + formatString + "'.");
247             }
248 
249             String output = format.format(hash);
250 
251             System.out.println(output);
252 
253         } catch (IllegalArgumentException iae) {
254             exit(iae, debug);
255         } catch (UnknownAlgorithmException uae) {
256             exit(uae, debug);
257         } catch (IOException ioe) {
258             exit(ioe, debug);
259         } catch (Exception e) {
260             printHelpAndExit(options, e, debug, -1);
261         } finally {
262             if (passwordChars != null && passwordChars.length > 0) {
263                 for (int i = 0; i < passwordChars.length; i++) {
264                     passwordChars[i] = ' ';
265                 }
266             }
267         }
268     }
269 
270     private static String createMutexMessage(Option... options) {
271         StringBuilder sb = new StringBuilder();
272         sb.append("The ");
273 
274         for (int i = 0; i < options.length; i++) {
275             if (i > 0) {
276                 sb.append(", ");
277             }
278             Option o = options[0];
279             sb.append("-").append(o.getOpt()).append("/--").append(o.getLongOpt());
280         }
281         sb.append(" and generated salt options are mutually exclusive.  Only one of them may be used at a time");
282         return sb.toString();
283     }
284 
285     private static void exit(Exception e, boolean debug) {
286         printException(e, debug);
287         System.exit(-1);
288     }
289 
290     private static int getRequiredPositiveInt(CommandLine line, Option option) {
291         String iterVal = line.getOptionValue(option.getOpt());
292         try {
293             return Integer.parseInt(iterVal);
294         } catch (NumberFormatException e) {
295             String msg = "'" + option.getLongOpt() + "' value must be a positive integer.";
296             throw new IllegalArgumentException(msg, e);
297         }
298     }
299 
300     private static ByteSource getSalt(String saltString, String saltBytesString, boolean generateSalt, int generatedSaltSize) {
301 
302         if (saltString != null) {
303             if (generateSalt || (saltBytesString != null)) {
304                 throw new IllegalArgumentException(SALT_MUTEX_MSG);
305             }
306             return ByteSource.Util.bytes(saltString);
307         }
308 
309         if (saltBytesString != null) {
310             if (generateSalt) {
311                 throw new IllegalArgumentException(SALT_MUTEX_MSG);
312             }
313 
314             String value = saltBytesString;
315             boolean base64 = true;
316             if (saltBytesString.startsWith(HEX_PREFIX)) {
317                 //hex:
318                 base64 = false;
319                 value = value.substring(HEX_PREFIX.length());
320             }
321             byte[] bytes;
322             if (base64) {
323                 bytes = Base64.decode(value);
324             } else {
325                 bytes = Hex.decode(value);
326             }
327             return ByteSource.Util.bytes(bytes);
328         }
329 
330         if (generateSalt) {
331             SecureRandomNumberGeneratoror.html#SecureRandomNumberGenerator">SecureRandomNumberGenerator generator = new SecureRandomNumberGenerator();
332             int byteSize = generatedSaltSize / 8; //generatedSaltSize is in *bits* - convert to byte size:
333             return generator.nextBytes(byteSize);
334         }
335 
336         //no salt used:
337         return null;
338     }
339 
340     private static void printException(Exception e, boolean debug) {
341         if (e != null) {
342             System.out.println();
343             if (debug) {
344                 System.out.println("Error: ");
345                 e.printStackTrace(System.out);
346                 System.out.println(e.getMessage());
347 
348             } else {
349                 System.out.println("Error: " + e.getMessage());
350                 System.out.println();
351                 System.out.println("Specify -d or --debug for more information.");
352             }
353         }
354     }
355 
356     private static void printHelp(Options options, Exception e, boolean debug) {
357         HelpFormatter help = new HelpFormatter();
358         String command = "java -jar shiro-tools-hasher-<version>.jar [options] [<value>]";
359         String header = "\nPrint a cryptographic hash (aka message digest) of the specified <value>.\n--\nOptions:";
360         String footer = "\n" +
361                 "<value> is optional only when hashing passwords (see below).  It is\n" +
362                 "required all other times." +
363                 "\n\n" +
364                 "Password Hashing:\n" +
365                 "---------------------------------\n" +
366                 "Specify the -p/--password option and DO NOT enter a <value>.  You will\n" +
367                 "be prompted for a password and characters will not echo as you type." +
368                 "\n\n" +
369                 "Salting:\n" +
370                 "---------------------------------\n" +
371                 "Specifying a salt:" +
372                 "\n\n" +
373                 "You may specify a salt using the -s/--salt option followed by the salt\n" +
374                 "value.  If the salt value is a base64 or hex string representing a\n" +
375                 "byte array, you must specify the -sb/--saltbytes option to indicate this,\n" +
376                 "otherwise the text value bytes will be used directly." +
377                 "\n\n" +
378                 "When using -sb/--saltbytes, the -s/--salt value is expected to be a\n" +
379                 "base64-encoded string by default.  If the value is a hex-encoded string,\n" +
380                 "you must prefix the string with 0x (zero x) to indicate a hex value." +
381                 "\n\n" +
382                 "Generating a salt:" +
383                 "\n\n" +
384                 "Use the -gs/--gensalt option if you don't want to specify a salt,\n" +
385                 "but want a strong random salt to be generated and used during hashing.\n" +
386                 "The generated salt size defaults to 128 bits.  You may specify\n" +
387                 "a different size by using the -gss/--gensaltsize option followed by\n" +
388                 "a positive integer (size is in bits, not bytes)." +
389                 "\n\n" +
390                 "Because a salt must be specified if computing the hash later,\n" +
391                 "generated salts are only useful with the shiro1 output format;\n" +
392                 "the other formats do not include the generated salt." +
393                 "\n\n" +
394                 "Specifying a private salt:" +
395                 "\n\n" +
396                 "You may specify a private salt using the -ps/--privatesalt option followed\n" +
397                 "by the private salt value.  If the private salt value is a base64 or hex \n" +
398                 "string representing a byte array, you must specify the -psb/--privatesaltbytes\n" +
399                 "option to indicate this, otherwise the text value bytes will be used directly." +
400                 "\n\n" +
401                 "When using -psb/--privatesaltbytes, the -ps/--privatesalt value is expected to\n" +
402                 "be a base64-encoded string by default.  If the value is a hex-encoded string,\n" +
403                 "you must prefix the string with 0x (zero x) to indicate a hex value." +
404                 "\n\n" +
405                 "Files, URLs and classpath resources:\n" +
406                 "---------------------------------\n" +
407                 "If using the -r/--resource option, the <value> represents a resource path.\n" +
408                 "By default this is expected to be a file path, but you may specify\n" +
409                 "classpath or URL resources by using the classpath: or url: prefix\n" +
410                 "respectively." +
411                 "\n\n" +
412                 "Some examples:" +
413                 "\n\n" +
414                 "<command> -r fileInCurrentDirectory.txt\n" +
415                 "<command> -r ../../relativePathFile.xml\n" +
416                 "<command> -r ~/documents/myfile.pdf\n" +
417                 "<command> -r /usr/local/logs/absolutePathFile.log\n" +
418                 "<command> -r url:http://foo.com/page.html\n" +
419                 "<command> -r classpath:/WEB-INF/lib/something.jar" +
420                 "\n\n" +
421                 "Output Format:\n" +
422                 "---------------------------------\n" +
423                 "Specify the -f/--format option followed by either 1) the format ID (as defined\n" +
424                 "by the " + DefaultHashFormatFactory.class.getName() + "\n" +
425                 "JavaDoc) or 2) the fully qualified " + HashFormat.class.getName() + "\n" +
426                 "implementation class name to instantiate and use for formatting.\n\n" +
427                 "The default output format is 'shiro1' which is a Modular Crypt Format (MCF)\n" +
428                 "that shows all relevant information as a dollar-sign ($) delimited string.\n" +
429                 "This format is ideal for use in Shiro's text-based user configuration (e.g.\n" +
430                 "shiro.ini or a properties file).";
431 
432         printException(e, debug);
433 
434         System.out.println();
435         help.printHelp(command, header, options, null);
436         System.out.println(footer);
437     }
438 
439     private static void printHelpAndExit(Options options, Exception e, boolean debug, int exitCode) {
440         printHelp(options, e, debug);
441         System.exit(exitCode);
442     }
443 
444     private static char[] readPassword(boolean confirm) {
445         java.io.Console console = System.console();
446         if (console == null) {
447             throw new IllegalStateException("java.io.Console is not available on the current JVM.  Cannot read passwords.");
448         }
449         char[] first = console.readPassword("%s", "Password to hash: ");
450         if (first == null || first.length == 0) {
451             throw new IllegalArgumentException("No password specified.");
452         }
453         if (confirm) {
454             char[] second = console.readPassword("%s", "Password to hash (confirm): ");
455             if (!Arrays.equals(first, second)) {
456                 String msg = "Password entries do not match.";
457                 throw new IllegalArgumentException(msg);
458             }
459         }
460         return first;
461     }
462 
463     private static File toFile(String path) {
464         String resolved = path;
465         if (path.startsWith("~/") || path.startsWith(("~\\"))) {
466             resolved = path.replaceFirst("\\~", System.getProperty("user.home"));
467         }
468         return new File(resolved);
469     }
470 
471     private static String toString(String[] strings) {
472         int len = strings != null ? strings.length : 0;
473         if (len == 0) {
474             return null;
475         }
476         return StringUtils.toDelimitedString(strings, " ");
477     }
478 }