001    /*
002     * Licensed to the Apache Software Foundation (ASF) under one
003     * or more contributor license agreements.  See the NOTICE file
004     * distributed with this work for additional information
005     * regarding copyright ownership.  The ASF licenses this file
006     * to you under the Apache License, Version 2.0 (the
007     * "License"); you may not use this file except in compliance
008     * with the License.  You may obtain a copy of the License at
009     *
010     *     http://www.apache.org/licenses/LICENSE-2.0
011     *
012     * Unless required by applicable law or agreed to in writing,
013     * software distributed under the License is distributed on an
014     * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
015     * KIND, either express or implied.  See the License for the
016     * specific language governing permissions and limitations
017     * under the License.
018     */
019    package org.apache.shiro.tools.hasher;
020    
021    import org.apache.commons.cli.*;
022    import org.apache.shiro.authc.credential.DefaultPasswordService;
023    import org.apache.shiro.codec.Base64;
024    import org.apache.shiro.codec.Hex;
025    import org.apache.shiro.crypto.SecureRandomNumberGenerator;
026    import org.apache.shiro.crypto.UnknownAlgorithmException;
027    import org.apache.shiro.crypto.hash.SimpleHash;
028    import org.apache.shiro.crypto.hash.format.*;
029    import org.apache.shiro.io.ResourceUtils;
030    import org.apache.shiro.util.ByteSource;
031    import org.apache.shiro.util.JavaEnvironment;
032    import org.apache.shiro.util.StringUtils;
033    
034    import java.io.File;
035    import java.io.IOException;
036    import java.util.Arrays;
037    
038    /**
039     * Commandline line utility to hash data such as strings, passwords, resources (files, urls, etc).
040     * <p/>
041     * Usage:
042     * <pre>
043     * java -jar shiro-tools-hasher<em>-version</em>-cli.jar
044     * </pre>
045     * This will print out all supported options with documentation.
046     *
047     * @since 1.2
048     */
049    public final class Hasher {
050    
051        private static final String HEX_PREFIX = "0x";
052        private static final String DEFAULT_ALGORITHM_NAME = "MD5";
053        private static final String DEFAULT_PASSWORD_ALGORITHM_NAME = DefaultPasswordService.DEFAULT_HASH_ALGORITHM;
054        private static final int DEFAULT_GENERATED_SALT_SIZE = 128;
055        private static final int DEFAULT_NUM_ITERATIONS = 1;
056        private static final int DEFAULT_PASSWORD_NUM_ITERATIONS = DefaultPasswordService.DEFAULT_HASH_ITERATIONS;
057    
058        private static final Option ALGORITHM = new Option("a", "algorithm", true, "hash algorithm name.  Defaults to SHA-256 when password hashing, MD5 otherwise.");
059        private static final Option DEBUG = new Option("d", "debug", false, "show additional error (stack trace) information.");
060        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.");
061        private static final Option HELP = new Option("help", "help", false, "show this help message.");
062        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.");
063        private static final Option PASSWORD = new Option("p", "password", false, "hash a password (disable typing echo)");
064        private static final Option PASSWORD_NC = new Option("pnc", "pnoconfirm", false, "hash a password (disable typing echo) but disable password confirmation prompt.");
065        private static final Option RESOURCE = new Option("r", "resource", false, "read and hash the resource located at <value>.  See below for more information.");
066        private static final Option SALT = new Option("s", "salt", true, "use the specified salt.  <arg> is plaintext.");
067        private static final Option SALT_BYTES = new Option("sb", "saltbytes", true, "use the specified salt bytes.  <arg> is hex or base64 encoded text.");
068        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.");
069        private static final Option NO_SALT_GEN = new Option("ngs", "nogensalt", false, "do NOT generate and use a random salt (valid during password hashing).");
070        private static final Option SALT_GEN_SIZE = new Option("gss", "gensaltsize", true, "the number of salt bits (not bytes!) to generate.  Defaults to 128.");
071    
072        private static final String SALT_MUTEX_MSG = createMutexMessage(SALT, SALT_BYTES);
073    
074        private static final HashFormatFactory HASH_FORMAT_FACTORY = new DefaultHashFormatFactory();
075    
076        static {
077            ALGORITHM.setArgName("name");
078            SALT_GEN_SIZE.setArgName("numBits");
079            ITERATIONS.setArgName("num");
080            SALT.setArgName("sval");
081            SALT_BYTES.setArgName("encTxt");
082        }
083    
084        public static void main(String[] args) {
085    
086            CommandLineParser parser = new PosixParser();
087    
088            Options options = new Options();
089            options.addOption(HELP).addOption(DEBUG).addOption(ALGORITHM).addOption(ITERATIONS);
090            options.addOption(RESOURCE).addOption(PASSWORD).addOption(PASSWORD_NC);
091            options.addOption(SALT).addOption(SALT_BYTES).addOption(SALT_GEN).addOption(SALT_GEN_SIZE).addOption(NO_SALT_GEN);
092            options.addOption(FORMAT);
093    
094            boolean debug = false;
095            String algorithm = null; //user unspecified
096            int iterations = 0; //0 means unspecified by the end-user
097            boolean resource = false;
098            boolean password = false;
099            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    }