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.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
51
52
53
54
55
56
57
58
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;
110 int iterations = 0;
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
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
235
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
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;
333 return generator.nextBytes(byteSize);
334 }
335
336
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 }