Author: lhazlewood
Date: Wed Mar 30 04:04:28 2011
New Revision: 1086831

URL: http://svn.apache.org/viewvc?rev=1086831&view=rev
Log:
SHIRO-279: added some additional options, added support for shiro .ini password 
format

Modified:
    
shiro/trunk/tools/hasher/src/main/java/org/apache/shiro/tools/hasher/Hasher.java

Modified: 
shiro/trunk/tools/hasher/src/main/java/org/apache/shiro/tools/hasher/Hasher.java
URL: 
http://svn.apache.org/viewvc/shiro/trunk/tools/hasher/src/main/java/org/apache/shiro/tools/hasher/Hasher.java?rev=1086831&r1=1086830&r2=1086831&view=diff
==============================================================================
--- 
shiro/trunk/tools/hasher/src/main/java/org/apache/shiro/tools/hasher/Hasher.java
 (original)
+++ 
shiro/trunk/tools/hasher/src/main/java/org/apache/shiro/tools/hasher/Hasher.java
 Wed Mar 30 04:04:28 2011
@@ -19,10 +19,15 @@
 package org.apache.shiro.tools.hasher;
 
 import org.apache.commons.cli.*;
+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.io.ResourceUtils;
+import org.apache.shiro.util.ByteSource;
 import org.apache.shiro.util.JavaEnvironment;
+import org.apache.shiro.util.SimpleByteSource;
 import org.apache.shiro.util.StringUtils;
 
 import java.io.File;
@@ -34,7 +39,7 @@ import java.util.Arrays;
  * <p/>
  * Usage:
  * <pre>
- * java -jar shiro-tools-hasher<em>-version</em>.jar
+ * java -jar shiro-tools-hasher<em>-version</em>-cli.jar
  * </pre>
  * This will print out all supported options with documentation.
  *
@@ -42,26 +47,27 @@ import java.util.Arrays;
  */
 public final class Hasher {
 
+    private static final Option ALGORITHM = new Option("a", "algorithm", true, 
"hash algorithm name.  Defaults to MD5.");
+    private static final Option DEBUG = new Option("d", "debug", false, "show 
additional error (stack trace) information.");
+    private static final Option HELP = new Option("help", "help", false, "show 
this help message.");
+    private static final Option HEX = new Option("h", "hex", false, "display a 
hex value instead of Base64.");
+    private static final Option ITERATIONS = new Option("i", "iterations", 
true, "number of hash iterations.  Defaults to 1.");
+    private static final Option NO_FORMAT = new Option("nf", "noformat", 
false, "turn off output formatting.  Any generated salt will be placed after 
the hash separated by a space.");
+    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, "disable password hash 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.");
+    private static final Option SALT_GEN_HEX = new Option("gsh", "gensalthex", 
false, "display the generated salt's hex value instead of Base64.");
+    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 Option SHIRO = new Option("shiro", "shiro", false, 
"display output in the Shiro password file format (.ini [users] config).");
 
+    private static final String HEX_PREFIX = "0x";
     private static final String DEFAULT_ALGORITHM_NAME = "MD5";
+    private static final int DEFAULT_GENERATED_SALT_SIZE = 128;
     private static final int DEFAULT_NUM_ITERATIONS = 1;
-
-    private static final String ALG_OPT = "a";
-    private static final String ALG_OPT_LONG = "algorithm";
-    private static final String DEBUG_OPT = "d";
-    private static final String DEBUG_OPT_LONG = "debug"; //show stack traces 
if there are any.
-    private static final String ITER_OPT = "i";
-    private static final String ITER_OPT_LONG = "iterations";
-    private static final String HEX_OPT = "h";
-    private static final String HEX_OPT_LONG = "hex";
-    private static final String HELP_OPT = "help";
-    private static final String HELP_OPT_LONG = "help";
-    private static final String PASSWORD_OPT = "p";
-    private static final String PASSWORD_OPT_LONG = "password";
-    private static final String PASSWORD_OPT_NOCONFIRM = "pnc";
-    private static final String PASSWORD_OPT_NOCONFIRM_LONG = "pnoconfirm";
-    private static final String RESOURCE_OPT = "r";
-    private static final String RESOURCE_OPT_LONG = "resource";
+    private static final String SALT_MUTEX_MSG = createMutexMessage(SALT, 
SALT_BYTES);
 
 
     public static void main(String[] args) {
@@ -69,14 +75,10 @@ public final class Hasher {
         CommandLineParser parser = new PosixParser();
 
         Options options = new Options();
-        options.addOption(ALG_OPT, ALG_OPT_LONG, true, "hash algorithm name.  
Defaults to MD5.");
-        options.addOption(ITER_OPT, ITER_OPT_LONG, true, "number of hash 
iterations.  Defaults to 1.");
-        options.addOption(HEX_OPT, HEX_OPT_LONG, false, "print hex value.  
Defaults to Base64.");
-        options.addOption(HELP_OPT, HELP_OPT_LONG, false, "print this help 
message.");
-        options.addOption(DEBUG_OPT, DEBUG_OPT_LONG, false, "show additional 
error (stack trace) information.");
-        options.addOption(PASSWORD_OPT, PASSWORD_OPT_LONG, false, "hash a 
password (do not echo).");
-        options.addOption(RESOURCE_OPT, RESOURCE_OPT_LONG, false, "read and 
hash the resource located at <value>. See below for more information.");
-        options.addOption(PASSWORD_OPT_NOCONFIRM, PASSWORD_OPT_NOCONFIRM_LONG, 
false, "disable confirmation prompt for password hashing.");
+        
options.addOption(HELP).addOption(DEBUG).addOption(ALGORITHM).addOption(HEX).addOption(ITERATIONS);
+        options.addOption(RESOURCE).addOption(PASSWORD).addOption(PASSWORD_NC);
+        
options.addOption(SALT).addOption(SALT_BYTES).addOption(SALT_GEN).addOption(SALT_GEN_SIZE).addOption(SALT_GEN_HEX);
+        options.addOption(NO_FORMAT).addOption(SHIRO);
 
         boolean debug = false;
         String algorithm = DEFAULT_ALGORITHM_NAME;
@@ -85,36 +87,72 @@ public final class Hasher {
         boolean resource = false;
         boolean password = false;
         boolean passwordConfirm = true;
+        String saltString = null;
+        String saltBytesString = null;
+        boolean generateSalt = false;
+        boolean generatedSaltBase64 = true;
+        int generatedSaltSize = DEFAULT_GENERATED_SALT_SIZE;
+
+        boolean shiroFormat = false;
+        boolean format = true;
 
         char[] passwordChars = null;
 
         try {
             CommandLine line = parser.parse(options, args);
 
-            if (line.hasOption(HELP_OPT)) {
+            if (line.hasOption(HELP.getOpt())) {
                 printHelpAndExit(options, null, debug, 0);
             }
-            if (line.hasOption(DEBUG_OPT)) {
+            if (line.hasOption(DEBUG.getOpt())) {
                 debug = true;
             }
-            if (line.hasOption(ALG_OPT)) {
-                algorithm = line.getOptionValue(ALG_OPT);
+            if (line.hasOption(ALGORITHM.getOpt())) {
+                algorithm = line.getOptionValue(ALGORITHM.getOpt());
             }
-            if (line.hasOption(ITER_OPT)) {
-                iterations = getRequiredPositiveInt(line, ITER_OPT, 
ITER_OPT_LONG);
+            if (line.hasOption(ITERATIONS.getOpt())) {
+                iterations = getRequiredPositiveInt(line, ITERATIONS);
             }
-            if (line.hasOption(HEX_OPT)) {
+            if (line.hasOption(HEX.getOpt())) {
                 base64 = false;
             }
-            if (line.hasOption(PASSWORD_OPT)) {
+            if (line.hasOption(PASSWORD.getOpt())) {
                 password = true;
             }
-            if (line.hasOption(RESOURCE_OPT)) {
+            if (line.hasOption(RESOURCE.getOpt())) {
                 resource = true;
             }
-            if (line.hasOption(PASSWORD_OPT_NOCONFIRM)) {
+            if (line.hasOption(PASSWORD_NC.getOpt())) {
                 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(SALT_GEN.getOpt())) {
+                generateSalt = true;
+            }
+            if (line.hasOption(SALT_GEN_HEX.getOpt())) {
+                generateSalt = true;
+                generatedSaltBase64 = false;
+            }
+            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(NO_FORMAT.getOpt())) {
+                format = false;
+            }
+            if (line.hasOption(SHIRO.getOpt())) {
+                shiroFormat = true;
+            }
+
+            String sourceValue = null;
 
             Object source;
 
@@ -128,25 +166,34 @@ public final class Hasher {
                 }
 
                 assert remainingArgs != null;
-                String value = toString(remainingArgs);
+                sourceValue = toString(remainingArgs);
 
                 if (resource) {
-                    if (!ResourceUtils.hasResourcePrefix(value)) {
-                        source = toFile(value);
+                    if (!ResourceUtils.hasResourcePrefix(sourceValue)) {
+                        source = toFile(sourceValue);
                     } else {
-                        source = ResourceUtils.getInputStreamForPath(value);
+                        source = 
ResourceUtils.getInputStreamForPath(sourceValue);
                     }
                 } else {
-                    source = value;
+                    source = sourceValue;
                 }
             }
 
-            SimpleHash hash = new SimpleHash(algorithm, source, /* salt not 
supported yet*/ null, iterations);
-            if (base64) {
-                System.out.println(hash.toBase64());
+            ByteSource salt = getSalt(saltString, saltBytesString, 
generateSalt, generatedSaltSize);
+
+            SimpleHash hash = new SimpleHash(algorithm, source, salt, 
iterations);
+
+            StringBuilder output;
+            if (shiroFormat) {
+                output = formatForShiroIni(hash, base64, salt, 
generatedSaltBase64, generateSalt);
+            } else if (format) {
+                output = format(hash, base64, salt, generatedSaltBase64, 
generateSalt, algorithm, sourceValue);
             } else {
-                System.out.println(hash.toHex());
+                output = formatMinimal(hash, base64, salt, 
generatedSaltBase64, generateSalt);
             }
+
+            System.out.println(output);
+
         } catch (IllegalArgumentException iae) {
             exit(iae, debug);
         } catch (UnknownAlgorithmException uae) {
@@ -164,54 +211,156 @@ public final class Hasher {
         }
     }
 
-    private static String toString(String[] strings) {
-        int len = strings != null ? strings.length : 0;
-        if (len == 0) {
-            return null;
+    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());
         }
-        return StringUtils.toDelimitedString(strings, " ");
+        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 StringBuilder format(ByteSource hash, boolean hashBase64, 
ByteSource salt, boolean saltBase64, boolean showSalt, String alg, String 
value) {
+        StringBuilder sb = new StringBuilder();
+
+        sb.append(alg).append("(").append(value).append(")");
+
+        if (hashBase64) {
+            sb.append(" base64 = ").append(hash.toBase64());
+        } else {
+            sb.append(" hex = ").append(hash.toHex());
+        }
+
+        if (showSalt && salt != null) {
+            sb.append("\nGenerated salt");
+            if (saltBase64) {
+                sb.append(" base64 = ").append(salt.toBase64());
+            } else {
+                sb.append(" hex = ").append(salt.toHex());
+            }
+        }
+
+        return sb;
+    }
+
+    private static StringBuilder formatForShiroIni(ByteSource hash, boolean 
hashBase64, ByteSource salt, boolean saltBase64, boolean showSalt) {
+        StringBuilder sb = new StringBuilder();
+
+        if (hashBase64) {
+            sb.append(hash.toBase64());
+        } else {
+            //hex:
+            sb.append(HEX_PREFIX).append(hash.toHex());
+        }
+
+        if (showSalt && salt != null) {
+            sb.append(" ");
+            if (saltBase64) {
+                sb.append(salt.toBase64());
+            } else {
+                //hex:
+                sb.append(HEX_PREFIX).append(salt.toHex());
+            }
+        }
+        return sb;
+    }
+
+    private static StringBuilder formatMinimal(ByteSource hash, boolean 
hashBase64, ByteSource salt, boolean saltBase64, boolean showSalt) {
+        StringBuilder sb = new StringBuilder();
+
+        if (hashBase64) {
+            sb.append(hash.toBase64());
+        } else {
+            sb.append(hash.toHex());
+        }
+
+        if (showSalt && salt != null) {
+            sb.append(" ");
+            if (saltBase64) {
+                sb.append(salt.toBase64());
+            } else {
+                sb.append(salt.toHex());
+            }
+        }
+
+        return sb;
     }
 
-    private static int getRequiredPositiveInt(CommandLine line, String opt, 
String optLong) {
-        String iterVal = line.getOptionValue(opt);
+    private static int getRequiredPositiveInt(CommandLine line, Option option) 
{
+        String iterVal = line.getOptionValue(option.getOpt());
         try {
             return Integer.parseInt(iterVal);
         } catch (NumberFormatException e) {
-            String msg = "'" + optLong + "' value must be a positive integer.";
+            String msg = "'" + option.getLongOpt() + "' value must be a 
positive integer.";
             throw new IllegalArgumentException(msg, e);
         }
     }
 
-    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 ByteSource getSalt(String saltString, String 
saltBytesString, boolean generateSalt, int generatedSaltSize) {
 
-    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);
+        if (saltString != null) {
+            if (generateSalt || (saltBytesString != null)) {
+                throw new IllegalArgumentException(SALT_MUTEX_MSG);
+            }
+            return new SimpleByteSource(saltString);
         }
-        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.");
+
+        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 new SimpleByteSource(bytes);
         }
-        char[] first = console.readPassword("%s", "Password to hash: ");
-        if (first == null || first.length == 0) {
-            throw new IllegalArgumentException("No password specified.");
+
+        if (generateSalt) {
+            SecureRandomNumberGenerator generator = new 
SecureRandomNumberGenerator();
+            int byteSize = generatedSaltSize / 8; //generatedSaltSize is in 
*bits* - convert to byte size:
+            return generator.nextBytes(byteSize);
         }
-        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);
+
+        //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.");
             }
         }
-        return first;
     }
 
     private static void printHelp(Options options, Exception e, boolean debug) 
{
@@ -220,19 +369,52 @@ public final class Hasher {
         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" +
+                "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" +
-                "Files, URLs and classpath resources:\n--\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 bytes.  You may 
specify\n" +
+                "a different size by using the -sgs/--saltgeneratedsize option 
followed by\n" +
+                "a positive integer." +
+                "\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" +
+                "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 ~/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";
@@ -244,29 +426,56 @@ public final class Hasher {
         System.out.println(footer);
     }
 
-    private static void exit(Exception e, boolean debug) {
-        printException(e, debug);
-        System.exit(-1);
+    private static void printHelpAndExit(Options options, Exception e, boolean 
debug, int exitCode) {
+        printHelp(options, e, debug);
+        System.exit(exitCode);
     }
 
-    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 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 void printHelpAndExit(Options options, Exception e, boolean 
debug, int exitCode) {
-        printHelp(options, e, debug);
-        System.exit(exitCode);
+    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, " ");
+    }
+
+    static {
+        ALGORITHM.setArgName("name");
+        SALT_GEN_SIZE.setArgName("numBits");
+        ITERATIONS.setArgName("num");
+        SALT.setArgName("sval");
+        SALT_BYTES.setArgName("encTxt");
     }
 }


Reply via email to