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.*;
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
40
41
42
43
44
45
46
47
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;
96 int iterations = 0;
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
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
209
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
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;
307 return generator.nextBytes(byteSize);
308 }
309
310
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 }