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