Hasher.java
/*
* 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, " ");
}
}