Coverage Report - org.apache.shiro.tools.hasher.Hasher
 
Classes in this File Line Coverage Branch Coverage Complexity
Hasher
0%
0/200
0%
0/106
7.364
 
 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.*;
 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  
  * Commandline line utility to hash data such as strings, passwords, resources (files, urls, etc).
 40  
  * <p/>
 41  
  * Usage:
 42  
  * <pre>
 43  
  * java -jar shiro-tools-hasher<em>-version</em>-cli.jar
 44  
  * </pre>
 45  
  * This will print out all supported options with documentation.
 46  
  *
 47  
  * @since 1.2
 48  
  */
 49  0
 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  0
     private static final Option ALGORITHM = new Option("a", "algorithm", true, "hash algorithm name.  Defaults to SHA-256 when password hashing, MD5 otherwise.");
 59  0
     private static final Option DEBUG = new Option("d", "debug", false, "show additional error (stack trace) information.");
 60  0
     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  0
     private static final Option HELP = new Option("help", "help", false, "show this help message.");
 62  0
     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  0
     private static final Option PASSWORD = new Option("p", "password", false, "hash a password (disable typing echo)");
 64  0
     private static final Option PASSWORD_NC = new Option("pnc", "pnoconfirm", false, "hash a password (disable typing echo) but disable password confirmation prompt.");
 65  0
     private static final Option RESOURCE = new Option("r", "resource", false, "read and hash the resource located at <value>.  See below for more information.");
 66  0
     private static final Option SALT = new Option("s", "salt", true, "use the specified salt.  <arg> is plaintext.");
 67  0
     private static final Option SALT_BYTES = new Option("sb", "saltbytes", true, "use the specified salt bytes.  <arg> is hex or base64 encoded text.");
 68  0
     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  0
     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  0
     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  0
     private static final String SALT_MUTEX_MSG = createMutexMessage(SALT, SALT_BYTES);
 73  
 
 74  0
     private static final HashFormatFactory HASH_FORMAT_FACTORY = new DefaultHashFormatFactory();
 75  
 
 76  
     static {
 77  0
         ALGORITHM.setArgName("name");
 78  0
         SALT_GEN_SIZE.setArgName("numBits");
 79  0
         ITERATIONS.setArgName("num");
 80  0
         SALT.setArgName("sval");
 81  0
         SALT_BYTES.setArgName("encTxt");
 82  0
     }
 83  
 
 84  
     public static void main(String[] args) {
 85  
 
 86  0
         CommandLineParser parser = new PosixParser();
 87  
 
 88  0
         Options options = new Options();
 89  0
         options.addOption(HELP).addOption(DEBUG).addOption(ALGORITHM).addOption(ITERATIONS);
 90  0
         options.addOption(RESOURCE).addOption(PASSWORD).addOption(PASSWORD_NC);
 91  0
         options.addOption(SALT).addOption(SALT_BYTES).addOption(SALT_GEN).addOption(SALT_GEN_SIZE).addOption(NO_SALT_GEN);
 92  0
         options.addOption(FORMAT);
 93  
 
 94  0
         boolean debug = false;
 95  0
         String algorithm = null; //user unspecified
 96  0
         int iterations = 0; //0 means unspecified by the end-user
 97  0
         boolean resource = false;
 98  0
         boolean password = false;
 99  0
         boolean passwordConfirm = true;
 100  0
         String saltString = null;
 101  0
         String saltBytesString = null;
 102  0
         boolean generateSalt = false;
 103  0
         int generatedSaltSize = DEFAULT_GENERATED_SALT_SIZE;
 104  
 
 105  0
         String formatString = null;
 106  
 
 107  0
         char[] passwordChars = null;
 108  
 
 109  
         try {
 110  0
             CommandLine line = parser.parse(options, args);
 111  
 
 112  0
             if (line.hasOption(HELP.getOpt())) {
 113  0
                 printHelpAndExit(options, null, debug, 0);
 114  
             }
 115  0
             if (line.hasOption(DEBUG.getOpt())) {
 116  0
                 debug = true;
 117  
             }
 118  0
             if (line.hasOption(ALGORITHM.getOpt())) {
 119  0
                 algorithm = line.getOptionValue(ALGORITHM.getOpt());
 120  
             }
 121  0
             if (line.hasOption(ITERATIONS.getOpt())) {
 122  0
                 iterations = getRequiredPositiveInt(line, ITERATIONS);
 123  
             }
 124  0
             if (line.hasOption(PASSWORD.getOpt())) {
 125  0
                 password = true;
 126  0
                 generateSalt = true;
 127  
             }
 128  0
             if (line.hasOption(RESOURCE.getOpt())) {
 129  0
                 resource = true;
 130  
             }
 131  0
             if (line.hasOption(PASSWORD_NC.getOpt())) {
 132  0
                 password = true;
 133  0
                 generateSalt = true;
 134  0
                 passwordConfirm = false;
 135  
             }
 136  0
             if (line.hasOption(SALT.getOpt())) {
 137  0
                 saltString = line.getOptionValue(SALT.getOpt());
 138  
             }
 139  0
             if (line.hasOption(SALT_BYTES.getOpt())) {
 140  0
                 saltBytesString = line.getOptionValue(SALT_BYTES.getOpt());
 141  
             }
 142  0
             if (line.hasOption(NO_SALT_GEN.getOpt())) {
 143  0
                 generateSalt = false;
 144  
             }
 145  0
             if (line.hasOption(SALT_GEN.getOpt())) {
 146  0
                 generateSalt = true;
 147  
             }
 148  0
             if (line.hasOption(SALT_GEN_SIZE.getOpt())) {
 149  0
                 generateSalt = true;
 150  0
                 generatedSaltSize = getRequiredPositiveInt(line, SALT_GEN_SIZE);
 151  0
                 if (generatedSaltSize % 8 != 0) {
 152  0
                     throw new IllegalArgumentException("Generated salt size must be a multiple of 8 (e.g. 128, 192, 256, 512, etc).");
 153  
                 }
 154  
             }
 155  0
             if (line.hasOption(FORMAT.getOpt())) {
 156  0
                 formatString = line.getOptionValue(FORMAT.getOpt());
 157  
             }
 158  
 
 159  
             String sourceValue;
 160  
 
 161  
             Object source;
 162  
 
 163  0
             if (password) {
 164  0
                 passwordChars = readPassword(passwordConfirm);
 165  0
                 source = passwordChars;
 166  
             } else {
 167  0
                 String[] remainingArgs = line.getArgs();
 168  0
                 if (remainingArgs == null || remainingArgs.length != 1) {
 169  0
                     printHelpAndExit(options, null, debug, -1);
 170  
                 }
 171  
 
 172  0
                 assert remainingArgs != null;
 173  0
                 sourceValue = toString(remainingArgs);
 174  
 
 175  0
                 if (resource) {
 176  0
                     if (!ResourceUtils.hasResourcePrefix(sourceValue)) {
 177  0
                         source = toFile(sourceValue);
 178  
                     } else {
 179  0
                         source = ResourceUtils.getInputStreamForPath(sourceValue);
 180  
                     }
 181  
                 } else {
 182  0
                     source = sourceValue;
 183  
                 }
 184  
             }
 185  
 
 186  0
             if (algorithm == null) {
 187  0
                 if (password) {
 188  0
                     algorithm = DEFAULT_PASSWORD_ALGORITHM_NAME;
 189  
                 } else {
 190  0
                     algorithm = DEFAULT_ALGORITHM_NAME;
 191  
                 }
 192  
             }
 193  
 
 194  0
             if (iterations < DEFAULT_NUM_ITERATIONS) {
 195  
                 //Iterations were not specified.  Default to 350,000 when password hashing, and 1 for everything else:
 196  0
                 if (password) {
 197  0
                     iterations = DEFAULT_PASSWORD_NUM_ITERATIONS;
 198  
                 } else {
 199  0
                     iterations = DEFAULT_NUM_ITERATIONS;
 200  
                 }
 201  
             }
 202  
 
 203  0
             ByteSource salt = getSalt(saltString, saltBytesString, generateSalt, generatedSaltSize);
 204  
 
 205  0
             SimpleHash hash = new SimpleHash(algorithm, source, salt, iterations);
 206  
 
 207  0
             if (formatString == null) {
 208  
                 //Output format was not specified.  Default to 'shiro1' when password hashing, and 'hex' for
 209  
                 //everything else:
 210  0
                 if (password) {
 211  0
                     formatString = Shiro1CryptFormat.class.getName();
 212  
                 } else {
 213  0
                     formatString = HexFormat.class.getName();
 214  
                 }
 215  
             }
 216  
 
 217  0
             HashFormat format = HASH_FORMAT_FACTORY.getInstance(formatString);
 218  
 
 219  0
             if (format == null) {
 220  0
                 throw new IllegalArgumentException("Unrecognized hash format '" + formatString + "'.");
 221  
             }
 222  
 
 223  0
             String output = format.format(hash);
 224  
 
 225  0
             System.out.println(output);
 226  
 
 227  0
         } catch (IllegalArgumentException iae) {
 228  0
             exit(iae, debug);
 229  0
         } catch (UnknownAlgorithmException uae) {
 230  0
             exit(uae, debug);
 231  0
         } catch (IOException ioe) {
 232  0
             exit(ioe, debug);
 233  0
         } catch (Exception e) {
 234  0
             printHelpAndExit(options, e, debug, -1);
 235  
         } finally {
 236  0
             if (passwordChars != null && passwordChars.length > 0) {
 237  0
                 for (int i = 0; i < passwordChars.length; i++) {
 238  0
                     passwordChars[i] = ' ';
 239  
                 }
 240  
             }
 241  
         }
 242  0
     }
 243  
 
 244  
     private static String createMutexMessage(Option... options) {
 245  0
         StringBuilder sb = new StringBuilder();
 246  0
         sb.append("The ");
 247  
 
 248  0
         for (int i = 0; i < options.length; i++) {
 249  0
             if (i > 0) {
 250  0
                 sb.append(", ");
 251  
             }
 252  0
             Option o = options[0];
 253  0
             sb.append("-").append(o.getOpt()).append("/--").append(o.getLongOpt());
 254  
         }
 255  0
         sb.append(" and generated salt options are mutually exclusive.  Only one of them may be used at a time");
 256  0
         return sb.toString();
 257  
     }
 258  
 
 259  
     private static void exit(Exception e, boolean debug) {
 260  0
         printException(e, debug);
 261  0
         System.exit(-1);
 262  0
     }
 263  
 
 264  
     private static int getRequiredPositiveInt(CommandLine line, Option option) {
 265  0
         String iterVal = line.getOptionValue(option.getOpt());
 266  
         try {
 267  0
             return Integer.parseInt(iterVal);
 268  0
         } catch (NumberFormatException e) {
 269  0
             String msg = "'" + option.getLongOpt() + "' value must be a positive integer.";
 270  0
             throw new IllegalArgumentException(msg, e);
 271  
         }
 272  
     }
 273  
 
 274  
     private static ByteSource getSalt(String saltString, String saltBytesString, boolean generateSalt, int generatedSaltSize) {
 275  
 
 276  0
         if (saltString != null) {
 277  0
             if (generateSalt || (saltBytesString != null)) {
 278  0
                 throw new IllegalArgumentException(SALT_MUTEX_MSG);
 279  
             }
 280  0
             return ByteSource.Util.bytes(saltString);
 281  
         }
 282  
 
 283  0
         if (saltBytesString != null) {
 284  0
             if (generateSalt) {
 285  0
                 throw new IllegalArgumentException(SALT_MUTEX_MSG);
 286  
             }
 287  
 
 288  0
             String value = saltBytesString;
 289  0
             boolean base64 = true;
 290  0
             if (saltBytesString.startsWith(HEX_PREFIX)) {
 291  
                 //hex:
 292  0
                 base64 = false;
 293  0
                 value = value.substring(HEX_PREFIX.length());
 294  
             }
 295  
             byte[] bytes;
 296  0
             if (base64) {
 297  0
                 bytes = Base64.decode(value);
 298  
             } else {
 299  0
                 bytes = Hex.decode(value);
 300  
             }
 301  0
             return ByteSource.Util.bytes(bytes);
 302  
         }
 303  
 
 304  0
         if (generateSalt) {
 305  0
             SecureRandomNumberGenerator generator = new SecureRandomNumberGenerator();
 306  0
             int byteSize = generatedSaltSize / 8; //generatedSaltSize is in *bits* - convert to byte size:
 307  0
             return generator.nextBytes(byteSize);
 308  
         }
 309  
 
 310  
         //no salt used:
 311  0
         return null;
 312  
     }
 313  
 
 314  
     private static void printException(Exception e, boolean debug) {
 315  0
         if (e != null) {
 316  0
             System.out.println();
 317  0
             if (debug) {
 318  0
                 System.out.println("Error: ");
 319  0
                 e.printStackTrace(System.out);
 320  0
                 System.out.println(e.getMessage());
 321  
 
 322  
             } else {
 323  0
                 System.out.println("Error: " + e.getMessage());
 324  0
                 System.out.println();
 325  0
                 System.out.println("Specify -d or --debug for more information.");
 326  
             }
 327  
         }
 328  0
     }
 329  
 
 330  
     private static void printHelp(Options options, Exception e, boolean debug) {
 331  0
         HelpFormatter help = new HelpFormatter();
 332  0
         String command = "java -jar shiro-tools-hasher-<version>.jar [options] [<value>]";
 333  0
         String header = "\nPrint a cryptographic hash (aka message digest) of the specified <value>.\n--\nOptions:";
 334  0
         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  0
         printException(e, debug);
 397  
 
 398  0
         System.out.println();
 399  0
         help.printHelp(command, header, options, null);
 400  0
         System.out.println(footer);
 401  0
     }
 402  
 
 403  
     private static void printHelpAndExit(Options options, Exception e, boolean debug, int exitCode) {
 404  0
         printHelp(options, e, debug);
 405  0
         System.exit(exitCode);
 406  0
     }
 407  
 
 408  
     private static char[] readPassword(boolean confirm) {
 409  0
         if (!JavaEnvironment.isAtLeastVersion16()) {
 410  0
             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  0
             throw new IllegalArgumentException(msg);
 413  
         }
 414  0
         java.io.Console console = System.console();
 415  0
         if (console == null) {
 416  0
             throw new IllegalStateException("java.io.Console is not available on the current JVM.  Cannot read passwords.");
 417  
         }
 418  0
         char[] first = console.readPassword("%s", "Password to hash: ");
 419  0
         if (first == null || first.length == 0) {
 420  0
             throw new IllegalArgumentException("No password specified.");
 421  
         }
 422  0
         if (confirm) {
 423  0
             char[] second = console.readPassword("%s", "Password to hash (confirm): ");
 424  0
             if (!Arrays.equals(first, second)) {
 425  0
                 String msg = "Password entries do not match.";
 426  0
                 throw new IllegalArgumentException(msg);
 427  
             }
 428  
         }
 429  0
         return first;
 430  
     }
 431  
 
 432  
     private static File toFile(String path) {
 433  0
         String resolved = path;
 434  0
         if (path.startsWith("~/") || path.startsWith(("~\\"))) {
 435  0
             resolved = path.replaceFirst("\\~", System.getProperty("user.home"));
 436  
         }
 437  0
         return new File(resolved);
 438  
     }
 439  
 
 440  
     private static String toString(String[] strings) {
 441  0
         int len = strings != null ? strings.length : 0;
 442  0
         if (len == 0) {
 443  0
             return null;
 444  
         }
 445  0
         return StringUtils.toDelimitedString(strings, " ");
 446  
     }
 447  
 }