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