| /* |
| * Licensed to the Apache Software Foundation (ASF) under one |
| * or more contributor license agreements. See the NOTICE file |
| * distributed with this work for additional information |
| * regarding copyright ownership. The ASF licenses this file |
| * to you under the Apache License, Version 2.0 (the |
| * "License"); you may not use this file except in compliance |
| * with the License. You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, |
| * software distributed under the License is distributed on an |
| * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY |
| * KIND, either express or implied. See the License for the |
| * specific language governing permissions and limitations |
| * under the License. |
| */ |
| package org.apache.shiro.tools.hasher; |
| |
| import org.apache.commons.cli.*; |
| import org.apache.shiro.authc.credential.DefaultPasswordService; |
| import org.apache.shiro.codec.Base64; |
| import org.apache.shiro.codec.Hex; |
| import org.apache.shiro.crypto.SecureRandomNumberGenerator; |
| import org.apache.shiro.crypto.UnknownAlgorithmException; |
| import org.apache.shiro.crypto.hash.SimpleHash; |
| import org.apache.shiro.crypto.hash.format.*; |
| import org.apache.shiro.io.ResourceUtils; |
| import org.apache.shiro.util.ByteSource; |
| import org.apache.shiro.util.JavaEnvironment; |
| import org.apache.shiro.util.StringUtils; |
| |
| import java.io.File; |
| import java.io.IOException; |
| import java.util.Arrays; |
| |
| /** |
| * Commandline line utility to hash data such as strings, passwords, resources (files, urls, etc). |
| * <p/> |
| * Usage: |
| * <pre> |
| * java -jar shiro-tools-hasher<em>-version</em>-cli.jar |
| * </pre> |
| * This will print out all supported options with documentation. |
| * |
| * @since 1.2 |
| */ |
| public final class Hasher { |
| |
| private static final String HEX_PREFIX = "0x"; |
| private static final String DEFAULT_ALGORITHM_NAME = "MD5"; |
| private static final String DEFAULT_PASSWORD_ALGORITHM_NAME = DefaultPasswordService.DEFAULT_HASH_ALGORITHM; |
| private static final int DEFAULT_GENERATED_SALT_SIZE = 128; |
| private static final int DEFAULT_NUM_ITERATIONS = 1; |
| private static final int DEFAULT_PASSWORD_NUM_ITERATIONS = DefaultPasswordService.DEFAULT_HASH_ITERATIONS; |
| |
| private static final Option ALGORITHM = new Option("a", "algorithm", true, "hash algorithm name. Defaults to SHA-256 when password hashing, MD5 otherwise."); |
| private static final Option DEBUG = new Option("d", "debug", false, "show additional error (stack trace) information."); |
| 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."); |
| private static final Option HELP = new Option("help", "help", false, "show this help message."); |
| 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."); |
| private static final Option PASSWORD = new Option("p", "password", false, "hash a password (disable typing echo)"); |
| private static final Option PASSWORD_NC = new Option("pnc", "pnoconfirm", false, "hash a password (disable typing echo) but disable password confirmation prompt."); |
| private static final Option RESOURCE = new Option("r", "resource", false, "read and hash the resource located at <value>. See below for more information."); |
| private static final Option SALT = new Option("s", "salt", true, "use the specified salt. <arg> is plaintext."); |
| private static final Option SALT_BYTES = new Option("sb", "saltbytes", true, "use the specified salt bytes. <arg> is hex or base64 encoded text."); |
| 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."); |
| private static final Option NO_SALT_GEN = new Option("ngs", "nogensalt", false, "do NOT generate and use a random salt (valid during password hashing)."); |
| private static final Option SALT_GEN_SIZE = new Option("gss", "gensaltsize", true, "the number of salt bits (not bytes!) to generate. Defaults to 128."); |
| |
| private static final String SALT_MUTEX_MSG = createMutexMessage(SALT, SALT_BYTES); |
| |
| private static final HashFormatFactory HASH_FORMAT_FACTORY = new DefaultHashFormatFactory(); |
| |
| static { |
| ALGORITHM.setArgName("name"); |
| SALT_GEN_SIZE.setArgName("numBits"); |
| ITERATIONS.setArgName("num"); |
| SALT.setArgName("sval"); |
| SALT_BYTES.setArgName("encTxt"); |
| } |
| |
| public static void main(String[] args) { |
| |
| CommandLineParser parser = new PosixParser(); |
| |
| Options options = new Options(); |
| options.addOption(HELP).addOption(DEBUG).addOption(ALGORITHM).addOption(ITERATIONS); |
| options.addOption(RESOURCE).addOption(PASSWORD).addOption(PASSWORD_NC); |
| options.addOption(SALT).addOption(SALT_BYTES).addOption(SALT_GEN).addOption(SALT_GEN_SIZE).addOption(NO_SALT_GEN); |
| options.addOption(FORMAT); |
| |
| boolean debug = false; |
| String algorithm = null; //user unspecified |
| int iterations = 0; //0 means unspecified by the end-user |
| boolean resource = false; |
| boolean password = false; |
| boolean passwordConfirm = true; |
| String saltString = null; |
| String saltBytesString = null; |
| boolean generateSalt = false; |
| int generatedSaltSize = DEFAULT_GENERATED_SALT_SIZE; |
| |
| String formatString = null; |
| |
| char[] passwordChars = null; |
| |
| try { |
| CommandLine line = parser.parse(options, args); |
| |
| if (line.hasOption(HELP.getOpt())) { |
| printHelpAndExit(options, null, debug, 0); |
| } |
| if (line.hasOption(DEBUG.getOpt())) { |
| debug = true; |
| } |
| if (line.hasOption(ALGORITHM.getOpt())) { |
| algorithm = line.getOptionValue(ALGORITHM.getOpt()); |
| } |
| if (line.hasOption(ITERATIONS.getOpt())) { |
| iterations = getRequiredPositiveInt(line, ITERATIONS); |
| } |
| if (line.hasOption(PASSWORD.getOpt())) { |
| password = true; |
| generateSalt = true; |
| } |
| if (line.hasOption(RESOURCE.getOpt())) { |
| resource = true; |
| } |
| if (line.hasOption(PASSWORD_NC.getOpt())) { |
| password = true; |
| generateSalt = true; |
| passwordConfirm = false; |
| } |
| if (line.hasOption(SALT.getOpt())) { |
| saltString = line.getOptionValue(SALT.getOpt()); |
| } |
| if (line.hasOption(SALT_BYTES.getOpt())) { |
| saltBytesString = line.getOptionValue(SALT_BYTES.getOpt()); |
| } |
| if (line.hasOption(NO_SALT_GEN.getOpt())) { |
| generateSalt = false; |
| } |
| if (line.hasOption(SALT_GEN.getOpt())) { |
| generateSalt = true; |
| } |
| if (line.hasOption(SALT_GEN_SIZE.getOpt())) { |
| generateSalt = true; |
| generatedSaltSize = getRequiredPositiveInt(line, SALT_GEN_SIZE); |
| if (generatedSaltSize % 8 != 0) { |
| throw new IllegalArgumentException("Generated salt size must be a multiple of 8 (e.g. 128, 192, 256, 512, etc)."); |
| } |
| } |
| if (line.hasOption(FORMAT.getOpt())) { |
| formatString = line.getOptionValue(FORMAT.getOpt()); |
| } |
| |
| String sourceValue; |
| |
| Object source; |
| |
| if (password) { |
| passwordChars = readPassword(passwordConfirm); |
| source = passwordChars; |
| } else { |
| String[] remainingArgs = line.getArgs(); |
| if (remainingArgs == null || remainingArgs.length != 1) { |
| printHelpAndExit(options, null, debug, -1); |
| } |
| |
| assert remainingArgs != null; |
| sourceValue = toString(remainingArgs); |
| |
| if (resource) { |
| if (!ResourceUtils.hasResourcePrefix(sourceValue)) { |
| source = toFile(sourceValue); |
| } else { |
| source = ResourceUtils.getInputStreamForPath(sourceValue); |
| } |
| } else { |
| source = sourceValue; |
| } |
| } |
| |
| if (algorithm == null) { |
| if (password) { |
| algorithm = DEFAULT_PASSWORD_ALGORITHM_NAME; |
| } else { |
| algorithm = DEFAULT_ALGORITHM_NAME; |
| } |
| } |
| |
| if (iterations < DEFAULT_NUM_ITERATIONS) { |
| //Iterations were not specified. Default to 350,000 when password hashing, and 1 for everything else: |
| if (password) { |
| iterations = DEFAULT_PASSWORD_NUM_ITERATIONS; |
| } else { |
| iterations = DEFAULT_NUM_ITERATIONS; |
| } |
| } |
| |
| ByteSource salt = getSalt(saltString, saltBytesString, generateSalt, generatedSaltSize); |
| |
| SimpleHash hash = new SimpleHash(algorithm, source, salt, iterations); |
| |
| if (formatString == null) { |
| //Output format was not specified. Default to 'shiro1' when password hashing, and 'hex' for |
| //everything else: |
| if (password) { |
| formatString = Shiro1CryptFormat.class.getName(); |
| } else { |
| formatString = HexFormat.class.getName(); |
| } |
| } |
| |
| HashFormat format = HASH_FORMAT_FACTORY.getInstance(formatString); |
| |
| if (format == null) { |
| throw new IllegalArgumentException("Unrecognized hash format '" + formatString + "'."); |
| } |
| |
| String output = format.format(hash); |
| |
| System.out.println(output); |
| |
| } catch (IllegalArgumentException iae) { |
| exit(iae, debug); |
| } catch (UnknownAlgorithmException uae) { |
| exit(uae, debug); |
| } catch (IOException ioe) { |
| exit(ioe, debug); |
| } catch (Exception e) { |
| printHelpAndExit(options, e, debug, -1); |
| } finally { |
| if (passwordChars != null && passwordChars.length > 0) { |
| for (int i = 0; i < passwordChars.length; i++) { |
| passwordChars[i] = ' '; |
| } |
| } |
| } |
| } |
| |
| private static String createMutexMessage(Option... options) { |
| StringBuilder sb = new StringBuilder(); |
| sb.append("The "); |
| |
| for (int i = 0; i < options.length; i++) { |
| if (i > 0) { |
| sb.append(", "); |
| } |
| Option o = options[0]; |
| sb.append("-").append(o.getOpt()).append("/--").append(o.getLongOpt()); |
| } |
| sb.append(" and generated salt options are mutually exclusive. Only one of them may be used at a time"); |
| return sb.toString(); |
| } |
| |
| private static void exit(Exception e, boolean debug) { |
| printException(e, debug); |
| System.exit(-1); |
| } |
| |
| private static int getRequiredPositiveInt(CommandLine line, Option option) { |
| String iterVal = line.getOptionValue(option.getOpt()); |
| try { |
| return Integer.parseInt(iterVal); |
| } catch (NumberFormatException e) { |
| String msg = "'" + option.getLongOpt() + "' value must be a positive integer."; |
| throw new IllegalArgumentException(msg, e); |
| } |
| } |
| |
| private static ByteSource getSalt(String saltString, String saltBytesString, boolean generateSalt, int generatedSaltSize) { |
| |
| if (saltString != null) { |
| if (generateSalt || (saltBytesString != null)) { |
| throw new IllegalArgumentException(SALT_MUTEX_MSG); |
| } |
| return ByteSource.Util.bytes(saltString); |
| } |
| |
| if (saltBytesString != null) { |
| if (generateSalt) { |
| throw new IllegalArgumentException(SALT_MUTEX_MSG); |
| } |
| |
| String value = saltBytesString; |
| boolean base64 = true; |
| if (saltBytesString.startsWith(HEX_PREFIX)) { |
| //hex: |
| base64 = false; |
| value = value.substring(HEX_PREFIX.length()); |
| } |
| byte[] bytes; |
| if (base64) { |
| bytes = Base64.decode(value); |
| } else { |
| bytes = Hex.decode(value); |
| } |
| return ByteSource.Util.bytes(bytes); |
| } |
| |
| if (generateSalt) { |
| SecureRandomNumberGenerator generator = new SecureRandomNumberGenerator(); |
| int byteSize = generatedSaltSize / 8; //generatedSaltSize is in *bits* - convert to byte size: |
| return generator.nextBytes(byteSize); |
| } |
| |
| //no salt used: |
| return null; |
| } |
| |
| private static void printException(Exception e, boolean debug) { |
| if (e != null) { |
| System.out.println(); |
| if (debug) { |
| System.out.println("Error: "); |
| e.printStackTrace(System.out); |
| System.out.println(e.getMessage()); |
| |
| } else { |
| System.out.println("Error: " + e.getMessage()); |
| System.out.println(); |
| System.out.println("Specify -d or --debug for more information."); |
| } |
| } |
| } |
| |
| private static void printHelp(Options options, Exception e, boolean debug) { |
| HelpFormatter help = new HelpFormatter(); |
| String command = "java -jar shiro-tools-hasher-<version>.jar [options] [<value>]"; |
| String header = "\nPrint a cryptographic hash (aka message digest) of the specified <value>.\n--\nOptions:"; |
| String footer = "\n" + |
| "<value> is optional only when hashing passwords (see below). It is\n" + |
| "required all other times." + |
| "\n\n" + |
| "Password Hashing:\n" + |
| "---------------------------------\n" + |
| "Specify the -p/--password option and DO NOT enter a <value>. You will\n" + |
| "be prompted for a password and characters will not echo as you type." + |
| "\n\n" + |
| "Salting:\n" + |
| "---------------------------------\n" + |
| "Specifying a salt:" + |
| "\n\n" + |
| "You may specify a salt using the -s/--salt option followed by the salt\n" + |
| "value. If the salt value is a base64 or hex string representing a\n" + |
| "byte array, you must specify the -sb/--saltbytes option to indicate this,\n" + |
| "otherwise the text value bytes will be used directly." + |
| "\n\n" + |
| "When using -sb/--saltbytes, the -s/--salt value is expected to be a\n" + |
| "base64-encoded string by default. If the value is a hex-encoded string,\n" + |
| "you must prefix the string with 0x (zero x) to indicate a hex value." + |
| "\n\n" + |
| "Generating a salt:" + |
| "\n\n" + |
| "Use the -sg/--saltgenerated option if you don't want to specify a salt,\n" + |
| "but want a strong random salt to be generated and used during hashing.\n" + |
| "The generated salt size defaults to 128 bits. You may specify\n" + |
| "a different size by using the -sgs/--saltgeneratedsize option followed by\n" + |
| "a positive integer (size is in bits, not bytes)." + |
| "\n\n" + |
| "Because a salt must be specified if computing the\n" + |
| "hash later, generated salts will be printed, defaulting to base64\n" + |
| "encoding. If you prefer to use hex encoding, additionally use the\n" + |
| "-sgh/--saltgeneratedhex option." + |
| "\n\n" + |
| "Files, URLs and classpath resources:\n" + |
| "---------------------------------\n" + |
| "If using the -r/--resource option, the <value> represents a resource path.\n" + |
| "By default this is expected to be a file path, but you may specify\n" + |
| "classpath or URL resources by using the classpath: or url: prefix\n" + |
| "respectively." + |
| "\n\n" + |
| "Some examples:" + |
| "\n\n" + |
| "<command> -r fileInCurrentDirectory.txt\n" + |
| "<command> -r ../../relativePathFile.xml\n" + |
| "<command> -r ~/documents/myfile.pdf\n" + |
| "<command> -r /usr/local/logs/absolutePathFile.log\n" + |
| "<command> -r url:http://foo.com/page.html\n" + |
| "<command> -r classpath:/WEB-INF/lib/something.jar" + |
| "\n\n" + |
| "Output Format:\n" + |
| "---------------------------------\n" + |
| "Specify the -f/--format option followed by either 1) the format ID (as defined\n" + |
| "by the " + DefaultHashFormatFactory.class.getName() + "\n" + |
| "JavaDoc) or 2) the fully qualified " + HashFormat.class.getName() + "\n" + |
| "implementation class name to instantiate and use for formatting.\n\n" + |
| "The default output format is 'shiro1' which is a Modular Crypt Format (MCF)\n" + |
| "that shows all relevant information as a dollar-sign ($) delimited string.\n" + |
| "This format is ideal for use in Shiro's text-based user configuration (e.g.\n" + |
| "shiro.ini or a properties file)."; |
| |
| printException(e, debug); |
| |
| System.out.println(); |
| help.printHelp(command, header, options, null); |
| System.out.println(footer); |
| } |
| |
| private static void printHelpAndExit(Options options, Exception e, boolean debug, int exitCode) { |
| printHelp(options, e, debug); |
| System.exit(exitCode); |
| } |
| |
| private static char[] readPassword(boolean confirm) { |
| if (!JavaEnvironment.isAtLeastVersion16()) { |
| String msg = "Password hashing (prompt without echo) uses the java.io.Console to read passwords " + |
| "safely. This is only available on Java 1.6 platforms and later."; |
| throw new IllegalArgumentException(msg); |
| } |
| java.io.Console console = System.console(); |
| if (console == null) { |
| throw new IllegalStateException("java.io.Console is not available on the current JVM. Cannot read passwords."); |
| } |
| char[] first = console.readPassword("%s", "Password to hash: "); |
| if (first == null || first.length == 0) { |
| throw new IllegalArgumentException("No password specified."); |
| } |
| if (confirm) { |
| char[] second = console.readPassword("%s", "Password to hash (confirm): "); |
| if (!Arrays.equals(first, second)) { |
| String msg = "Password entries do not match."; |
| throw new IllegalArgumentException(msg); |
| } |
| } |
| return first; |
| } |
| |
| private static File toFile(String path) { |
| String resolved = path; |
| if (path.startsWith("~/") || path.startsWith(("~\\"))) { |
| resolved = path.replaceFirst("\\~", System.getProperty("user.home")); |
| } |
| return new File(resolved); |
| } |
| |
| private static String toString(String[] strings) { |
| int len = strings != null ? strings.length : 0; |
| if (len == 0) { |
| return null; |
| } |
| return StringUtils.toDelimitedString(strings, " "); |
| } |
| } |