1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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
58
59
60
61
62
63
64
65
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
138 String algorithm = null;
139
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
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
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
265
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
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
369 int byteSize = generatedSaltSize / 8;
370 return generator.nextBytes(byteSize);
371 }
372
373
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
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 }