jspwiki-commits mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From ajaqu...@apache.org
Subject svn commit: r656393 - in /incubator/jspwiki/trunk: src/com/ecyrd/jspwiki/util/CryptoUtil.java tests/com/ecyrd/jspwiki/util/CryptoUtilTest.java
Date Wed, 14 May 2008 20:02:32 GMT
Author: ajaquith
Date: Wed May 14 13:02:32 2008
New Revision: 656393

URL: http://svn.apache.org/viewvc?rev=656393&view=rev
Log:
Passwords are now salted and hashed per RFC 2307. Every password is salted with a 8-byte random
salt.

Added:
    incubator/jspwiki/trunk/src/com/ecyrd/jspwiki/util/CryptoUtil.java
    incubator/jspwiki/trunk/tests/com/ecyrd/jspwiki/util/CryptoUtilTest.java

Added: incubator/jspwiki/trunk/src/com/ecyrd/jspwiki/util/CryptoUtil.java
URL: http://svn.apache.org/viewvc/incubator/jspwiki/trunk/src/com/ecyrd/jspwiki/util/CryptoUtil.java?rev=656393&view=auto
==============================================================================
--- incubator/jspwiki/trunk/src/com/ecyrd/jspwiki/util/CryptoUtil.java (added)
+++ incubator/jspwiki/trunk/src/com/ecyrd/jspwiki/util/CryptoUtil.java Wed May 14 13:02:32
2008
@@ -0,0 +1,241 @@
+package com.ecyrd.jspwiki.util;
+
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.security.SecureRandom;
+import java.util.Arrays;
+import java.util.Random;
+
+import org.apache.commons.codec.binary.Base64;
+
+/**
+ * Hashes and verifies salted SHA-1 passwords, which are compliant with RFC
+ * 2307.
+ */
+public class CryptoUtil
+{
+
+    private static final String SSHA = "{SSHA}";
+
+    private static final Random RANDOM = new SecureRandom();
+
+    private static final int DEFAULT_SALT_SIZE = 8;
+
+    private static final Object HELP = "--help";
+
+    private static final Object HASH = "--hash";
+
+    private static final Object VERIFY = "--verify";
+
+    /**
+     * Private constructor to prevent direct instantiation.
+     */
+    private CryptoUtil()
+    {
+    }
+
+    /**
+     * <p>
+     * Convenience method for hashing and verifying salted SHA-1 passwords from
+     * the command line. This method requires <code>commons-codec-1.3.jar</code>
+     * (or a newer version) to be on the classpath. Command line arguments are
+     * as follows:
+     * </p>
+     * <ul>
+     * <li><code>--hash <var>password</var></code> - hashes
<var>password</var></code>
+     * and prints a password digest that looks like this: <blockquote><code>{SSHA}yfT8SRT/WoOuNuA6KbJeF10OznZmb28=</code></blockquote></li>
+     * <li><code>--verify <var>password</var> <var>digest</var></code>
-
+     * verifies <var>password</var> by extracting the salt from <var>digest</var>
+     * (which is identical to what is printed by <code>--hash</code>) and
+     * re-computing the digest again using the password and salt. If the
+     * password supplied is the same as the one used to create the original
+     * digest, <code>true</code> will be printed; otherwise <code>false</code></li>
+     * </ul>
+     * <p>For example, one way to use this utility is to change to JSPWiki's <code>build</code>
directory
+     * and type the following command:</p>
+     * <blockquote><code>java -cp JSPWiki.jar:../lib/commons-codec-1.3.jar com.ecyrd.jspwiki.util.CryptoUtil
--hash mynewpassword</code></blockquote>
+     * 
+     * @param args arguments for this method as described above
+     */
+    public static void main( String[] args ) throws Exception
+    {
+        // Print help if the user requested it, or if no arguments
+        if( args.length == 0 || (args.length == 1 && HELP.equals( args[0] )) )
+        {
+            System.out.println( "Usage: CryptUtil [options] " );
+            System.out.println( "   --hash   password             create hash for password"
);
+            System.out.println( "   --verify password digest      verify password for digest"
);
+            System.exit( 0 );
+        }
+
+        // User wants to hash the password
+        if( HASH.equals( args[0] ) )
+        {
+            if( args.length < 2 )
+            {
+                throw new IllegalArgumentException( "Error: --hash requires a 'password'
argument." );
+            }
+            String password = args[1].trim();
+            System.out.println( CryptoUtil.getSaltedPassword( password.getBytes() ) );
+        }
+
+        // User wants to verify an existing password
+        else if( VERIFY.equals( args[0] ) )
+        {
+            if( args.length < 3 )
+            {
+                throw new IllegalArgumentException( "Error: --hash requires 'password' and
'digest' arguments." );
+            }
+            String password = args[1].trim();
+            String digest = args[2].trim();
+            System.out.println( CryptoUtil.verifySaltedPassword( password.getBytes(), digest
) );
+        }
+
+        else
+        {
+            System.out.println( "Wrong usage. Try --help." );
+        }
+    }
+
+    /**
+     * <p>
+     * Creates an RFC 2307-compliant salted, hashed password with the SHA1
+     * MessageDigest algorithm. After the password is digested, the first 20
+     * bytes of the digest will be the actual password hash; the remaining bytes
+     * will be a randomly generated salt of length {@link #DEFAULT_SALT_SIZE},
+     * for example: <blockquote><code>{SSHA}3cGWem65NCEkF5Ew5AEk45ak8LHUWAwPVXAyyw==</code></blockquote>
+     * </p>
+     * <p>
+     * In layman's terms, the formula is
+     * <code>digest( secret + salt ) + salt</code>. The resulting digest is
+     * Base64-encoded.
+     * </p>
+     * <p>
+     * Note that successive invocations of this method with the same password
+     * will result in different hashes! (This, of course, is exactly the point.)
+     * </p>
+     * 
+     * @param password the password to be digested
+     * @return the Base64-encoded password hash, prepended by
+     *         <code>{SSHA}</code>.
+     */
+    public static String getSaltedPassword( byte[] password ) throws NoSuchAlgorithmException
+    {
+        byte[] salt = new byte[DEFAULT_SALT_SIZE];
+        RANDOM.nextBytes( salt );
+        return getSaltedPassword( password, salt );
+    }
+
+    /**
+     * <p>
+     * Helper method that creates an RFC 2307-compliant salted, hashed password with the
SHA1
+     * MessageDigest algorithm. After the password is digested, the first 20
+     * bytes of the digest will be the actual password hash; the remaining bytes
+     * will be the salt. Thus, supplying a password <code>testing123</code>
+     * and a random salt <code>foo</code> produces the hash:
+     * </p>
+     * <blockquote><code>{SSHA}yfT8SRT/WoOuNuA6KbJeF10OznZmb28=</code></blockquote>
+     * <p>
+     * In layman's terms, the formula is
+     * <code>digest( secret + salt ) + salt</code>. The resulting digest is Base64-encoded.</p>
+     * 
+     * @param password the password to be digested
+     * @param salt the random salt
+     * @return the Base64-encoded password hash, prepended by <code>{SSHA}</code>.
+     */
+    protected static String getSaltedPassword( byte[] password, byte[] salt ) throws NoSuchAlgorithmException
+    {
+        MessageDigest digest = MessageDigest.getInstance( "SHA" );
+        digest.update( password );
+        byte[] hash = digest.digest( salt );
+
+        // Create an array with the hash plus the salt
+        byte[] all = new byte[hash.length + salt.length];
+        for( int i = 0; i < hash.length; i++ )
+        {
+            all[i] = hash[i];
+        }
+        for( int i = 0; i < salt.length; i++ )
+        {
+            all[hash.length + i] = salt[i];
+        }
+        byte[] base64 = Base64.encodeBase64( all );
+        return SSHA + new String( base64 );
+    }
+
+    public static boolean verifySaltedPassword( byte[] password, String entry ) throws NoSuchAlgorithmException
+    {
+        // First, extract everything after {SSHA} and decode from Base64
+        if( !entry.startsWith( SSHA ) )
+        {
+            throw new IllegalArgumentException( "Hash not prefixed by {SSHA}; is it really
a salted hash?" );
+        }
+        byte[] challenge = Base64.decodeBase64( entry.substring( 6 ).getBytes() );
+
+        // Extract the password hash and salt
+        byte[] passwordHash = extractPasswordHash( challenge );
+        byte[] salt = extractSalt( challenge );
+
+        // Re-create the hash using the password and the extracted salt
+        MessageDigest digest = MessageDigest.getInstance( "SHA" );
+        digest.update( password );
+        byte[] hash = digest.digest( salt );
+
+        // See if our extracted hash matches what we just re-created
+        return Arrays.equals( passwordHash, hash );
+    }
+
+    /**
+     * Helper method that extracts the hashed password fragment from a supplied salted SHA
digest
+     * by taking all of the characters before position 20.
+     * 
+     * @param challenge the salted digest, which is assumed to have been
+     *            previously decoded from Base64.
+     * @return the password hash
+     * @throws IllegalArgumentException if the length of the supplied digest is
+     *             less than or equal to 20 bytes
+     */
+    protected static byte[] extractPasswordHash( byte[] digest )
+    {
+        if( digest.length < 20 )
+        {
+            throw new IllegalArgumentException( "Hash was less than 20 characters; could
not extract password hash!" );
+        }
+
+        // Extract the password hash
+        byte[] hash = new byte[20];
+        for( int i = 0; i < 20; i++ )
+        {
+            hash[i] = digest[i];
+        }
+
+        return hash;
+    }
+
+    /**
+     * Helper method that extracts the salt from supplied salted digest by taking all of
the
+     * characters at position 20 and higher.
+     * 
+     * @param digest the salted digest, which is assumed to have been previously
+     *            decoded from Base64.
+     * @return the salt
+     * @throws IllegalArgumentException if the length of the supplied digest is
+     *             less than or equal to 20 bytes
+     */
+    protected static byte[] extractSalt( byte[] digest )
+    {
+        if( digest.length <= 20 )
+        {
+            throw new IllegalArgumentException( "Hash was less than 21 characters; we found
no salt!" );
+        }
+
+        // Extract the salt
+        byte[] salt = new byte[digest.length - 20];
+        for( int i = 20; i < digest.length; i++ )
+        {
+            salt[i - 20] = digest[i];
+        }
+
+        return salt;
+    }
+}

Added: incubator/jspwiki/trunk/tests/com/ecyrd/jspwiki/util/CryptoUtilTest.java
URL: http://svn.apache.org/viewvc/incubator/jspwiki/trunk/tests/com/ecyrd/jspwiki/util/CryptoUtilTest.java?rev=656393&view=auto
==============================================================================
--- incubator/jspwiki/trunk/tests/com/ecyrd/jspwiki/util/CryptoUtilTest.java (added)
+++ incubator/jspwiki/trunk/tests/com/ecyrd/jspwiki/util/CryptoUtilTest.java Wed May 14 13:02:32
2008
@@ -0,0 +1,157 @@
+package com.ecyrd.jspwiki.util;
+
+import java.io.ByteArrayOutputStream;
+import java.io.OutputStream;
+import java.io.PrintStream;
+
+import junit.framework.Test;
+import junit.framework.TestCase;
+import junit.framework.TestSuite;
+
+import org.apache.commons.codec.binary.Base64;
+
+public class CryptoUtilTest extends TestCase
+{
+
+    public static Test suite()
+    {
+        return new TestSuite( CryptoUtilTest.class );
+    }
+
+    public void testCommandLineHash() throws Exception
+    {
+        // Save old printstream
+        PrintStream oldOut = System.out;
+
+        // Swallow System out and get command output
+        OutputStream out = new ByteArrayOutputStream();
+        System.setOut( new PrintStream( out ) );
+        CryptoUtil.main( new String[] { "--hash", "password" } );
+        String output = new String( out.toString() );
+
+        // Restore old printstream
+        System.setOut( oldOut );
+
+        // Run our tests
+        assertTrue( output.startsWith( "{SSHA}" ) );
+    }
+
+    public void testCommandLineNoVerify() throws Exception
+    {
+        // Save old printstream
+        PrintStream oldOut = System.out;
+
+        // Swallow System out and get command output
+        OutputStream out = new ByteArrayOutputStream();
+        System.setOut( new PrintStream( out ) );
+        // Supply a bogus password
+        CryptoUtil.main( new String[] { "--verify", "password", "{SSHA}yfT8SRT/WoOuNuA6KbJeF10OznZmb28="
} );
+        String output = new String( out.toString() );
+
+        // Restore old printstream
+        System.setOut( oldOut );
+
+        // Run our tests
+        assertTrue( output.startsWith( "false" ) );
+    }
+    
+    public void testCommandLineSyntaxError1() throws Exception
+    {
+        // Try verifying password without the {SSHA} prefix
+        try {
+            CryptoUtil.main( new String[] { "--verify", "password", "yfT8SRT/WoOuNuA6KbJeF10OznZmb28="
} );
+        }
+        catch (IllegalArgumentException e)
+        {
+            // Excellent; we expected an error
+        }
+    }
+    
+    public void testCommandLineVerify() throws Exception
+    {
+        // Save old printstream
+        PrintStream oldOut = System.out;
+
+        // Swallow System out and get command output
+        OutputStream out = new ByteArrayOutputStream();
+        System.setOut( new PrintStream( out ) );
+        CryptoUtil.main( new String[] { "--verify", "testing123", "{SSHA}yfT8SRT/WoOuNuA6KbJeF10OznZmb28="
} );
+        String output = new String( out.toString() );
+
+        // Restore old printstream
+        System.setOut( oldOut );
+
+        // Run our tests
+        assertTrue( output.startsWith( "true" ) );
+    }
+
+    public void testExtractHash()
+    {
+        byte[] digest;
+
+        digest = Base64.decodeBase64( "yfT8SRT/WoOuNuA6KbJeF10OznZmb28=".getBytes() );
+        assertEquals( "foo", new String( CryptoUtil.extractSalt( digest ) ) );
+
+        digest = Base64.decodeBase64( "tAVisOOQGAeVyP8UMFQY9qi83lxsb09e".getBytes() );
+        assertEquals( "loO^", new String( CryptoUtil.extractSalt( digest ) ) );
+
+        digest = Base64.decodeBase64( "BZaDYvB8czmNW3MjR2j7/mklODV0ZXN0eQ==".getBytes() );
+        assertEquals( "testy", new String( CryptoUtil.extractSalt( digest ) ) );
+    }
+
+    public void testGetSaltedPassword() throws Exception
+    {
+        byte[] password;
+
+        // Generate a hash with a known password and salt
+        password = "testing123".getBytes();
+        assertEquals( "{SSHA}yfT8SRT/WoOuNuA6KbJeF10OznZmb28=", CryptoUtil.getSaltedPassword(
password, "foo".getBytes() ) );
+
+        // Generate two hashes with a known password and 2 different salts
+        password = "password".getBytes();
+        assertEquals( "{SSHA}tAVisOOQGAeVyP8UMFQY9qi83lxsb09e", CryptoUtil.getSaltedPassword(
password, "loO^".getBytes() ) );
+        assertEquals( "{SSHA}BZaDYvB8czmNW3MjR2j7/mklODV0ZXN0eQ==", CryptoUtil.getSaltedPassword(
password, "testy".getBytes() ) );
+    }
+
+    public void testMultipleHashes() throws Exception
+    {
+        String p1 = CryptoUtil.getSaltedPassword( "password".getBytes() );
+        String p2 = CryptoUtil.getSaltedPassword( "password".getBytes() );
+        String p3 = CryptoUtil.getSaltedPassword( "password".getBytes() );
+        assertNotSame( p1, p2 );
+        assertNotSame( p2, p3 );
+        assertNotSame( p1, p3 );
+    }
+
+    public void testSaltedPasswordLength() throws Exception
+    {
+        // Generate a hash with a known password and salt
+        byte[] password = "mySooperRandomPassword".getBytes();
+        String hash = CryptoUtil.getSaltedPassword( password, "salt".getBytes() );
+
+        // slappasswd says that a 4-byte salt should give us 6 chars for prefix
+        // + 20 chars for the hash + 12 for salt (38 total)
+        assertEquals( 38, hash.length() );
+    }
+
+    public void verifySaltedPassword() throws Exception
+    {
+        byte[] password;
+
+        // Verify with a known digest
+        password = "testing123".getBytes();
+        assertTrue( CryptoUtil.verifySaltedPassword( password, "{SSHA}yfT8SRT/WoOuNuA6KbJeF10OznZmb28="
) );
+
+        // Verify with two more known digests
+        password = "password".getBytes();
+        assertTrue( CryptoUtil.verifySaltedPassword( password, "{SSHA}tAVisOOQGAeVyP8UMFQY9qi83lxsb09e"
) );
+        assertTrue( CryptoUtil.verifySaltedPassword( password, "{SSHA}BZaDYvB8czmNW3MjR2j7/mklODV0ZXN0eQ=="
) );
+
+        // Verify with three consecutive random generations (based on
+        // slappasswd)
+        password = "testPassword".getBytes();
+        assertTrue( CryptoUtil.verifySaltedPassword( password, "{SSHA}t2tfJHm/QZYUh0OZ8tkm05l2LLbuc3ZF"
) );
+        assertTrue( CryptoUtil.verifySaltedPassword( password, "{SSHA}0FKV9iM2cA5bAMws7mSgwg+zik/GT+wy"
) );
+        assertTrue( CryptoUtil.verifySaltedPassword( password, "{SSHA}/0Dzvh+8+w0YO673Qr7vqEOmdeMSrbGG"
) );
+    }
+}



Mime
View raw message