diff --git a/RELEASE-NOTES b/RELEASE-NOTES index 6e70de234b..fb1f640bbb 100644 --- a/RELEASE-NOTES +++ b/RELEASE-NOTES @@ -33,8 +33,10 @@ Backwards Incompatible Changes exactly how the Session may be used for subject state persistence. This allows a single point of control rather than needing to configure Shiro in multiple places. - If you overrode this method in Shiro 1.0 or 1.2, please look at the new + If you overrode this method in Shiro 1.0 or 1.1, please look at the new org.apache.shiro.mgt.DefaultSubjectDAO implementation, which performs compatible logic. + Documentation for this is covered here: + http://shiro.apache.org/session-management.html#SessionManagement-SessionsandSubjectState - The org.apache.shiro.web.session.mgt.ServletContainerSessionManager implementation (enabled by default for all web applications) no longer subclasses @@ -48,6 +50,7 @@ Backwards Incompatible Changes be honored. It was better to remove the extends clause to ensure that any such configuration would fail fast when Shiro starts up to reflect the invalid config. + Potential Breaking Changes -------------------------------- - The org.apache.shiro.web.filter.mgt.FilterChainManager class's @@ -56,6 +59,40 @@ Potential Breaking Changes If you ever called this method, you can call the addFilter(name, filter, true) method to achieve the <= 1.1 behavior. +- The org.apache.shiro.crypto.SecureRandomNumberGenerator previously defaulted to generating + 128 random _bytes_ each time the nextBytes() method was called. This is too large for most purposes, so the + default has been changed to 16 _bytes_ (which equals 128 bits - what was originally intended). If for some reason + you need more than 16 bytes (128 bits) of randomly generated bits, you will need to configure the + 'defaultNextByteSize' property to match your desired size (in bytes, NOT bits). + +- Shiro's Block Cipher Services (AesCipherService, BlowfishCipherService) have had the following changes: + + 1) The internal Cipher Mode and Streaming Cipher Mode have been changed from CFB to the new default of CBC. + CBC is more commonly used for block ciphers today (e.g. SSL). + If you were using an AES or Blowfish CipherService you will want to revert to the previous defaults in your config + to ensure you can still decrypt previously encrypted data. For example, in code: + + blockCipherService.setMode(OperationMode.CFB); + blockCipherService.setStreamingMode(OperationMode.CFB); + + or, in shiro.ini: + + blockCipherService.modeName = CFB + blockCipherService.streamingModeName = CFB + + 2) The internal Streaming Padding Scheme has been changed from NONE to PKCS5 as PKCS5 is more commonly used. + If you were using an AES or Blowfish CipherService for streaming operations, you will want to revert to the + previous padding scheme default to ensure you can still decrypt previously encrypted data. For example, in code: + + blockCipherService.setStreamingPaddingScheme(PaddingScheme.NONE); + + or, in shiro.ini: + + blockCipherService.streamingPaddingSchemeName = NoPadding + + Note the difference in code vs shiro.ini in this last example: 'NoPadding' is the correct text value, 'NONE' is + the correct Enum value. + ########################################################### # 1.1.0 ########################################################### diff --git a/core/src/main/java/org/apache/shiro/authc/credential/DefaultPasswordService.java b/core/src/main/java/org/apache/shiro/authc/credential/DefaultPasswordService.java index b1c86965ea..0340dbb349 100644 --- a/core/src/main/java/org/apache/shiro/authc/credential/DefaultPasswordService.java +++ b/core/src/main/java/org/apache/shiro/authc/credential/DefaultPasswordService.java @@ -18,143 +18,156 @@ */ package org.apache.shiro.authc.credential; -import org.apache.shiro.authc.AuthenticationInfo; -import org.apache.shiro.authc.AuthenticationToken; -import org.apache.shiro.authc.SaltedAuthenticationInfo; import org.apache.shiro.codec.Base64; -import org.apache.shiro.codec.CodecSupport; -import org.apache.shiro.codec.Hex; -import org.apache.shiro.crypto.hash.*; +import org.apache.shiro.crypto.RandomNumberGenerator; +import org.apache.shiro.crypto.SecureRandomNumberGenerator; +import org.apache.shiro.crypto.hash.Hash; +import org.apache.shiro.crypto.hash.SimpleHash; import org.apache.shiro.util.ByteSource; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import java.util.Arrays; /** - * Default implementation of the {@link PasswordService} interface. Delegates to an internal (configurable) - * {@link Hasher} instance. + * Default implementation of the {@link PasswordService} interface. * * @since 1.2 */ public class DefaultPasswordService implements PasswordService { - private ConfigurableHasher hasher; + public static final String DEFAULT_HASH_ALGORITHM_NAME = "SHA-512"; + //see http://www.katasoft.com/blog/2011/04/04/strong-password-hashing-apache-shiro + public static final int DEFAULT_HASH_ITERATIONS = 500000; //500,000 + public static final int DEFAULT_SALT_SIZE = 32; //32 bytes == 256 bits - private String storedCredentialsEncoding = "base64"; + private static final String MCF_PREFIX = "$shiro1$"; //Modular Crypt Format prefix specific to Shiro's needs + + private static final Logger log = LoggerFactory.getLogger(DefaultPasswordService.class); + + private String hashAlgorithmName; + private int hashIterations; + private int saltSize; + private RandomNumberGenerator randomNumberGenerator; public DefaultPasswordService() { - this.hasher = new DefaultHasher(); - this.hasher.setHashAlgorithmName(Sha512Hash.ALGORITHM_NAME); - //see http://www.katasoft.com/blog/2011/04/04/strong-password-hashing-apache-shiro: - this.hasher.setHashIterations(200000); + this.hashAlgorithmName = DEFAULT_HASH_ALGORITHM_NAME; + this.hashIterations = DEFAULT_HASH_ITERATIONS; + this.saltSize = DEFAULT_SALT_SIZE; + this.randomNumberGenerator = new SecureRandomNumberGenerator(); } - public HashResponse hashPassword(ByteSource plaintextPassword) { - byte[] plaintextBytes = plaintextPassword != null ? plaintextPassword.getBytes() : null; - if (plaintextBytes == null || plaintextBytes.length == 0) { + public String hashPassword(String plaintext) { + if (plaintext == null || plaintext.length() == 0) { return null; } - - return this.hasher.computeHash(new SimpleHashRequest(plaintextPassword)); + return hashPassword(ByteSource.Util.bytes(plaintext)); } - public boolean doCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) { - - ByteSource publicSalt = null; - if (info instanceof SaltedAuthenticationInfo) { - publicSalt = ((SaltedAuthenticationInfo) info).getCredentialsSalt(); + public String hashPassword(ByteSource plaintext) { + if (plaintext == null) { + return null; } + byte[] plaintextBytes = plaintext.getBytes(); + if (plaintextBytes == null || plaintextBytes.length == 0) { + return null; + } + String algorithmName = getHashAlgorithmName(); + ByteSource salt = getRandomNumberGenerator().nextBytes(getSaltSize()); + int iterations = Math.max(1, getHashIterations()); - Hash tokenCredentialsHash = hashProvidedCredentials(token, publicSalt); - byte[] storedCredentialsBytes = getCredentialsBytes(info); + Hash result = new SimpleHash(algorithmName, plaintext, salt, iterations); - return Arrays.equals(tokenCredentialsHash.getBytes(), storedCredentialsBytes); + //Modular Crypt Format + //TODO: make this pluggable: + return new StringBuilder(MCF_PREFIX).append(algorithmName).append("$").append(iterations).append("$") + .append(salt.toBase64()).append("$").append(result.toBase64()).toString(); } - protected byte[] getCredentialsBytes(AuthenticationInfo info) { - Object credentials = info.getCredentials(); + public boolean passwordsMatch(ByteSource submittedPassword, String savedPassword) { + if (savedPassword == null) { + return isEmpty(submittedPassword); + } else { + return !isEmpty(submittedPassword) && doPasswordsMatch(submittedPassword, savedPassword); + } + } - byte[] bytes = new BytesHelper().getBytes(credentials); + private static boolean isEmpty(ByteSource source) { + return source == null || source.getBytes() == null || source.getBytes().length == 0; + } - if (this.storedCredentialsEncoding != null && - (credentials instanceof String || credentials instanceof char[])) { - assertEncodingSupported(this.storedCredentialsEncoding); - bytes = decode(bytes, this.storedCredentialsEncoding); + private boolean doPasswordsMatch(ByteSource submittedPassword, String savedPassword) { + if (!savedPassword.startsWith(MCF_PREFIX)) { + log.warn("Encountered unrecognized saved password format. Falling back to simple equality " + + "comparison. Use the PasswordService to hash new passwords as well as match them."); + return ByteSource.Util.bytes(savedPassword).equals(submittedPassword); } - return bytes; - } + String suffix = savedPassword.substring(MCF_PREFIX.length()); + String[] parts = suffix.split("\\$"); + + //last part is always the digest/checksum, Base64-encoded: + int i = parts.length-1; + String digestBase64 = parts[i--]; + //second-to-last part is always the salt, Base64-encoded: + String saltBase64 = parts[i--]; + String iterationsString = parts[i--]; + String algorithmName = parts[i--]; + + /*String timestampString = null; - protected byte[] decode(byte[] storedCredentials, String encodingName) { - if ("hex".equalsIgnoreCase(encodingName)) { - return Hex.decode(storedCredentials); - } else if ("base64".equalsIgnoreCase(encodingName) || - "base-64".equalsIgnoreCase(encodingName)) { - return Base64.decode(storedCredentials); + if (parts.length == 5) { + timestampString = parts[i--]; + } */ + + byte[] digest = Base64.decode(digestBase64); + + byte[] salt = Base64.decode(saltBase64); + int iterations; + try { + iterations = Integer.parseInt(iterationsString); + } catch (NumberFormatException e) { + log.error("Unable to parse saved password string: " + savedPassword, e); + throw e; } - throw new IllegalStateException("Unsupported encoding '" + encodingName + "'."); - } - protected Hash hashProvidedCredentials(AuthenticationToken token, ByteSource salt) { - Object credentials = token.getCredentials(); - byte[] credentialsBytes = new BytesHelper().getBytes(credentials); - ByteSource credentialsByteSource = ByteSource.Util.bytes(credentialsBytes); + //now compute the digest on the submitted password. If the resulting digest matches the saved digest, + //the password matches: - HashRequest request = new SimpleHashRequest(credentialsByteSource, salt); + Hash submittedHash = new SimpleHash(algorithmName, submittedPassword, salt, iterations); - HashResponse response = this.hasher.computeHash(request); + return Arrays.equals(digest, submittedHash.getBytes()); + } - return response.getHash(); + public String getHashAlgorithmName() { + return hashAlgorithmName; } - /** - * Returns {@code true} if the argument equals (ignoring case): - * - * {@code false} otherwise. - *

- * Subclasses should override this method as well as the {@link #decode(byte[], String)} method if other - * encodings should be supported. - * - * @param encodingName the name of the encoding to check. - * @return {@code } - */ - protected boolean isEncodingSupported(String encodingName) { - return "hex".equalsIgnoreCase(encodingName) || - "base64".equalsIgnoreCase(encodingName) || - "base-64".equalsIgnoreCase(encodingName); + public void setHashAlgorithmName(String hashAlgorithmName) { + this.hashAlgorithmName = hashAlgorithmName; } + public int getHashIterations() { + return hashIterations; + } - protected void assertEncodingSupported(String encodingName) throws IllegalArgumentException { - if (!isEncodingSupported(encodingName)) { - String msg = "Unsupported encoding '" + encodingName + "'. Please check for typos."; - throw new IllegalArgumentException(msg); - } + public void setHashIterations(int hashIterations) { + this.hashIterations = hashIterations; } - public ConfigurableHasher getHasher() { - return hasher; + public int getSaltSize() { + return saltSize; } - public void setHasher(ConfigurableHasher hasher) { - this.hasher = hasher; + public void setSaltSize(int saltSize) { + this.saltSize = saltSize; } - public void setStoredCredentialsEncoding(String storedCredentialsEncoding) { - if (storedCredentialsEncoding != null) { - assertEncodingSupported(storedCredentialsEncoding); - } - this.storedCredentialsEncoding = storedCredentialsEncoding; + public RandomNumberGenerator getRandomNumberGenerator() { + return randomNumberGenerator; } - //will probably be removed in Shiro 2.0. See SHIRO-203: - //https://issues.apache.org/jira/browse/SHIRO-203 - private static final class BytesHelper extends CodecSupport { - public byte[] getBytes(Object o) { - return toBytes(o); - } + public void setRandomNumberGenerator(RandomNumberGenerator randomNumberGenerator) { + this.randomNumberGenerator = randomNumberGenerator; } } diff --git a/core/src/main/java/org/apache/shiro/authc/credential/PasswordMatcher.java b/core/src/main/java/org/apache/shiro/authc/credential/PasswordMatcher.java new file mode 100644 index 0000000000..0951b927eb --- /dev/null +++ b/core/src/main/java/org/apache/shiro/authc/credential/PasswordMatcher.java @@ -0,0 +1,81 @@ +/* + * 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.authc.credential; + +import org.apache.shiro.authc.AuthenticationInfo; +import org.apache.shiro.authc.AuthenticationToken; +import org.apache.shiro.util.ByteSource; + +/** + * A {@link CredentialsMatcher} that employs best-practices comparisons for hashed text passwords. + *

+ * This implementation delegates to an internal {@link PasswordService} to perform the actual password + * comparison. This class is essentially a bridge between the generic CredentialsMatcher interface and the + * more specific {@code PasswordService} component. + * + * @since 1.2 + */ +public class PasswordMatcher implements CredentialsMatcher { + + private PasswordService passwordService; + + public boolean doCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) { + PasswordService service = ensurePasswordService(); + ByteSource submittedPassword = getSubmittedPassword(token); + String hashedPassword = getStoredHashedPassword(info); + return service.passwordsMatch(submittedPassword, hashedPassword); + } + + private PasswordService ensurePasswordService() { + PasswordService service = getPasswordService(); + if (service == null) { + String msg = "Required PasswordService has not been configured."; + throw new IllegalStateException(msg); + } + return service; + } + + protected ByteSource getSubmittedPassword(AuthenticationToken token) { + Object credentials = token.getCredentials(); + if (credentials == null) { + return null; + } + return ByteSource.Util.bytes(credentials); + } + + protected String getStoredHashedPassword(AuthenticationInfo storedAccountInfo) { + Object credentials = storedAccountInfo.getCredentials(); + if (credentials == null) { + return null; + } + if (!(credentials instanceof String)) { + String msg = "The stored account credentials is expected to be a String representation of a hashed password."; + throw new IllegalArgumentException(msg); + } + return (String)credentials; + } + + public PasswordService getPasswordService() { + return passwordService; + } + + public void setPasswordService(PasswordService passwordService) { + this.passwordService = passwordService; + } +} diff --git a/core/src/main/java/org/apache/shiro/authc/credential/PasswordService.java b/core/src/main/java/org/apache/shiro/authc/credential/PasswordService.java index 71d3389e00..06e5f6f3e3 100644 --- a/core/src/main/java/org/apache/shiro/authc/credential/PasswordService.java +++ b/core/src/main/java/org/apache/shiro/authc/credential/PasswordService.java @@ -18,7 +18,6 @@ */ package org.apache.shiro.authc.credential; -import org.apache.shiro.crypto.hash.HashResponse; import org.apache.shiro.util.ByteSource; /** @@ -26,72 +25,112 @@ *

* Most importantly, implementations of this interface are expected to employ best-practices to ensure that * passwords remain as safe as possible in application environments. - *

- * As this interface extends the CredentialsMatcher interface, it will perform credentials matching for password-based - * authentication attempts. However, this interface includes another additional method, - * {@link #hashPassword(org.apache.shiro.util.ByteSource)} which will hash a raw password value into a more - * secure hashed format. *

Usage

- * To use this service effectively, you must do the following: - *

- *

    - *
  1. Define an implementation of this interface in your Shiro configuration. For example, in {@code shiro.ini}: + * A {@code PasswordService} is used at two different times during an application's lifecycle: + * + *

    Account Creation or Password Reset

    + * Whenever you create a new user account or reset that account's password, we must translate the end-user submitted + * raw/plaintext password value to a string format that is much safer to store. You do that by calling the + * {@link #hashPassword(ByteSource)} method to create the safer hashed and formatted value. For + * example: *
    - * [main]
    + * ByteSource plaintextBytes = ByteSource.Util.bytes(submittedPlaintextPassword);
    + * String hashed = passwordService.hashPassword(plaintextBytes);
      * ...
    - * passwordService = org.apache.shiro.authc.credential.DefaultPasswordService
    + * userAccount.setHashedPassword(hashed);
    + * userAccount.save(); //create or update to your data store
      * 
    - *
  2. - *
  3. Configure the {@code passwordService} instance with the most secure settings based on your application's needs. - * See the {@link DefaultPasswordService DefaultPasswordService JavaDoc} for configuration options. For example: + * Be sure to save this hashed password in your data store and never the original/raw submitted password. + *

    Login Password Comparison

    + * Shiro performs the comparison during login automatically. Along with your {@code PasswordService}, you just + * have to configure a {@link PasswordMatcher} on a realm that has password-based accounts. During a login attempt, + * shiro will use the {@code PasswordMatcher} and the {@code PasswordService} to automatically compare submitted + * passwords. + *

    + * For example, if using Shiro's INI, here is how you might configure the PasswordMatcher and PasswordService: *

    + * [main]
      * ...
    - * passwordService.hasher.baseSalt = _some_random_base64_encoded_byte_array_
    - * passwordService.hasher.hashIterations = 250000
    - * ...
    - * 
    - *
  4. - *
  5. Wire the password service into the {@code Realm} that will query for password-based accounts. The realm - * implementation is usually a subclass of {@link org.apache.shiro.realm.AuthenticatingRealm AuthenticatingRealm}, which - * supports configuration of a {@link CredentialsMatcher} instance: - *
    + * passwordService = org.apache.shiro.authc.credential.DefaultPasswordService
    + * # configure the passwordService to use the hashing settings you desire
      * ...
    - * myRealm.credentialsMatcher = $passwordService
    + * passwordMatcher = org.apache.shiro.authc.credential.PasswordMatcher
    + * passwordMatcher.passwordService = $passwordService
      * ...
    + * # Finally, set the matcher on a realm that requires password matching for account authentication:
    + * myRealm = ...
    + * myRealm.credentialsMatcher = $passwordMatcher
      * 
    - *
  6. - *
  7. During your application's new-user or password-reset workflow (whenever a user submits to you a new plaintext - * password), call the {@link #hashPassword(org.apache.shiro.util.ByteSource)} method immediately to acquire the - * hashed version. Store the returned {@link org.apache.shiro.crypto.hash.HashResponse#getHash() hash} and - * {@link org.apache.shiro.crypto.hash.HashResponse#getSalt() salt} instance to your user data store (and NOT - * the original raw password).
  8. - *
  9. Ensure your corresponding Realm implementation (that was configured with the {@code PasswordService} as its - * credentialsMatcher above) returns instances of - * {@link org.apache.shiro.authc.SaltedAuthenticationInfo SaltedAuthenticationInfo} during authentication attempts - * (typically represented by a call to your realm's - * {@link org.apache.shiro.realm.AuthenticatingRealm#doGetAuthenticationInfo(org.apache.shiro.authc.AuthenticationToken) doGetAuthenticationInfo} - * method). Ensure the {@code SaltedAuthenticationInfo} instance you construct returns the saved hash and salt you - * saved from step #4.
  10. - *
- * If you perform these steps and configure the {@code PasswordService) appropriately, you can rest assured you will be - * using very strong password hashing techniques. * + * @see DefaultPasswordService + * @see PasswordMatcher * @since 1.2 */ -public interface PasswordService extends CredentialsMatcher { +public interface PasswordService { /** - * Hashes the specified plain text password (usually acquired from your application's 'new user' or 'password reset' - * workflow). After this call returns, you typically will store the returned - * response's {@link org.apache.shiro.crypto.hash.HashResponse#getHash() hash} and - * {@link org.apache.shiro.crypto.hash.HashResponse#getSalt() salt} with the corresponding user record (e.g. - * in a database). + * Hashes the specified plaintext password (usually acquired from your application's 'new user' or 'password reset' + * workflow). After this call returns, you typically will store the returned formatted String with the + * corresponding user record (e.g. as the 'password' or 'passwordHash' attribute). + *

+ * The String returned from this argument must be presented to the + * {@link #passwordsMatch(ByteSource, String) passwordsMatch} method when performing a + * password comparison check. + *

Usage

+ * The input argument type is a {@code ByteSource} to support either String or character array + * {@code (char[])} arguments; character arrays are often a safer way to represent passwords as they can be + * cleared/nulled-out after use. + *

+ * Regardless of your choice of using Strings or character arrays to represent submitted passwords, you can wrap + * either as a {@code ByteSource} by using {@link ByteSource.Util}, for example, when the passwords are captured as + * Strings: + *

+     * ByteSource passwordBytes = ByteSource.Util.bytes(submittedPasswordString);
+     * String formattedHashedValue = passwordService.hashPassword(passwordBytes);
+     * 
+ * or, identically, when captured as a character array: + *
+     * ByteSource passwordBytes = ByteSource.Util.bytes(submittedPasswordCharacterArray);
+     * String formattedHashedValue = passwordService.hashPassword(passwordBytes);
+     * 
+ *

+ * The resulting {@code formattedHashedValue} should be stored with the account to be retrieved later during a + * login attempt. For example: + *

+     * String formattedHashedValue = passwordService.hashPassword(passwordBytes);
+     * ...
+     * userAccount.setHashedPassword(formattedHashedValue);
+     * userAccount.save(); //create or update to your data store
+     * 
* - * @param plaintextPassword a plain text password, usually acquired from your application's 'new user' or 'password reset' - * workflow. - * @return the password hash and salt to be stored with the corresponding user record. + * @param plaintext a {@code ByteSource} encapsulating a plaintext password's bytes, usually acquired from your + * application's 'new user' or 'password reset' workflow. + * @return the hashed password, formatted for storage. */ - HashResponse hashPassword(ByteSource plaintextPassword); - + String hashPassword(ByteSource plaintext); + /** + * Returns {@code true} if the {@code submittedPlaintext} password matches the existing {@code saved} password, + * {@code false} otherwise. + *

Usage

+ * The {@code submittedPlaintext} argument is a {@code ByteSource} to support both String and character array + * arguments. Regardless of which you use to capture submitted passwords, you can wrap either as a + * {@code ByteSource} as follows: + *
+     * ByteSource submittedPasswordBytes = ByteSource.Util.bytes(submittedPasswordStringOrCharacterArray);
+     * passwordService.passwordsMatch(submittedPasswordBytes, formattedHashedPassword);
+     * 
+ * + * @param submittedPlaintext a raw/plaintext password submitted by an end user/Subject. + * @param saved the previously hashed and formatted password known to be associated with an account. + * This value must have been previously generated from the + * {@link #hashPassword(ByteSource) hashPassword} method (typically + * when the account is created or the account's password is reset). + * @return {@code true} if the {@code submittedPlaintext} password matches the existing {@code saved} password, + * {@code false} otherwise. + */ + boolean passwordsMatch(ByteSource submittedPlaintext, String saved); } diff --git a/core/src/main/java/org/apache/shiro/codec/H64.java b/core/src/main/java/org/apache/shiro/codec/H64.java new file mode 100644 index 0000000000..8c674c21a7 --- /dev/null +++ b/core/src/main/java/org/apache/shiro/codec/H64.java @@ -0,0 +1,129 @@ +/* + * 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. + */ +/* + * The apr_md5_encode() routine in the APR project's apr_md5.c file uses much + * code obtained from the FreeBSD 3.0 MD5 crypt() function, which is licenced + * as follows: + * ---------------------------------------------------------------------------- + * "THE BEER-WARE LICENSE" (Revision 42): + * wrote this file. As long as you retain this notice you + * can do whatever you want with this stuff. If we meet some day, and you think + * this stuff is worth it, you can buy me a beer in return. Poul-Henning Kamp + * ---------------------------------------------------------------------------- + */ +package org.apache.shiro.codec; + +import java.io.IOException; + +/** + * Codec for Unix Crypt-style encoding. While similar to + * Base64, it is not compatible with Base64. + *

+ * This implementation is based on encoding algorithms found in the Apache Portable Runtime library's + * apr_md5.c + * implementation for its {@code crypt}-style support. The APR team in turn received inspiration for its encoding + * implementation based on FreeBSD 3.0's {@code /usr/src/lib/libcrypt/crypt.c} implementation. The + * accompanying license headers have been retained at the top of this source file. + *

+ * This file and all that it contains is ASL 2.0 compatible. + * + * @since 1.2 + */ +public class H64 { + + private static final char[] itoa64 = "./0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz".toCharArray(); + + private static short toShort(byte b) { + return (short) (b & 0xff); + } + + private static int toInt(byte[] bytes, int offset, int numBytes) { + if (numBytes < 1 || numBytes > 4) { + throw new IllegalArgumentException("numBytes must be between 1 and 4."); + } + int val = toShort(bytes[offset]); //1st byte + for (int i = 1; i < numBytes; i++) { //any remaining bytes: + short s = toShort(bytes[offset + i]); + switch (i) { + case 1: val |= s << 8; break; + case 2: val |= s << 16; break; + case 3: val |= s << 24; break; + } + } + return val; + } + + /** + * Appends the specified character into the buffer, rethrowing any encountered + * {@link IOException} as an {@link IllegalStateException} (since this method is used for internal + * implementation needs and we only ever use StringBuilders, we should never encounter an IOException). + * + * @param buf the buffer to append to + * @param c the character to append. + */ + private static void append(Appendable buf, char c) { + try { + buf.append(c); + } catch (IOException e) { + throw new IllegalStateException("Unable to append character to internal buffer.", e); + } + } + + /** + * Encodes the specified integer to {@code numChars} H64-compatible characters and appends them into {@code buf}. + * + * @param value the integer to encode to H64-compatible characters + * @param buf the output buffer + * @param numChars the number of characters the value should be converted to. 3, 2 or 1. + */ + private static void encodeAndAppend(int value, Appendable buf, int numChars) { + for (int i = 0; i < numChars; i++) { + append(buf, itoa64[value & 0x3f]); + value >>= 6; + } + } + + /** + * Encodes the specified bytes to an {@code H64}-encoded String. + * + * @param bytes + * @return + */ + public static String encodeToString(byte[] bytes) { + if (bytes == null || bytes.length == 0) return null; + + StringBuilder buf = new StringBuilder(); + + int length = bytes.length; + int remainder = length % 3; + int i = 0; //starting byte + int last3ByteIndex = length - remainder; //last byte whose index is a multiple of 3 + + for(; i < last3ByteIndex; i += 3) { + int twentyFourBit = toInt(bytes, i, 3); + encodeAndAppend(twentyFourBit, buf, 4); + } + if (remainder > 0) { + //one or two bytes that we still need to encode: + int a = toInt(bytes, i, remainder); + encodeAndAppend(a, buf, remainder + 1); + } + return buf.toString(); + } +} diff --git a/core/src/main/java/org/apache/shiro/config/ReflectionBuilder.java b/core/src/main/java/org/apache/shiro/config/ReflectionBuilder.java index c0dddef3d2..efe14dd21d 100644 --- a/core/src/main/java/org/apache/shiro/config/ReflectionBuilder.java +++ b/core/src/main/java/org/apache/shiro/config/ReflectionBuilder.java @@ -22,11 +22,7 @@ import org.apache.commons.beanutils.PropertyUtils; import org.apache.shiro.codec.Base64; import org.apache.shiro.codec.Hex; -import org.apache.shiro.util.ClassUtils; -import org.apache.shiro.util.CollectionUtils; -import org.apache.shiro.util.Factory; -import org.apache.shiro.util.Nameable; -import org.apache.shiro.util.StringUtils; +import org.apache.shiro.util.*; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -361,6 +357,9 @@ protected void applyProperty(Object object, String propertyName, String stringVa value = toList(stringValue); } else if (isTypedProperty(object, propertyName, byte[].class)) { value = toBytes(stringValue); + } else if (isTypedProperty(object, propertyName, ByteSource.class)) { + byte[] bytes = toBytes(stringValue); + value = ByteSource.Util.bytes(bytes); } else { value = resolveValue(stringValue); } diff --git a/core/src/main/java/org/apache/shiro/crypto/SecureRandomNumberGenerator.java b/core/src/main/java/org/apache/shiro/crypto/SecureRandomNumberGenerator.java index 73425548de..5c132d0cef 100644 --- a/core/src/main/java/org/apache/shiro/crypto/SecureRandomNumberGenerator.java +++ b/core/src/main/java/org/apache/shiro/crypto/SecureRandomNumberGenerator.java @@ -34,14 +34,15 @@ */ public class SecureRandomNumberGenerator implements RandomNumberGenerator { - protected static final int DEFAULT_NEXT_BYTES_SIZE = 128; + protected static final int DEFAULT_NEXT_BYTES_SIZE = 16; //16 bytes == 128 bits (a common number in crypto) private int defaultNextBytesSize; private SecureRandom secureRandom; /** * Creates a new instance with a default backing {@link SecureRandom SecureRandom} and a - * {@link #getDefaultNextBytesSize() defaultNextBytesSize} of {@code 128}. + * {@link #getDefaultNextBytesSize() defaultNextBytesSize} of {@code 16}, which equals 128 bits, a size commonly + * used in cryptographic algorithms. */ public SecureRandomNumberGenerator() { this.defaultNextBytesSize = DEFAULT_NEXT_BYTES_SIZE; @@ -82,7 +83,7 @@ public void setSecureRandom(SecureRandom random) throws NullPointerException { /** * Returns the size of the generated byte array for calls to {@link #nextBytes() nextBytes()}. Defaults to - * {@code 128}, a commonly used number in cryptographic algorithms. + * {@code 16}, which equals 128 bits, a size commonly used in cryptographic algorithms. * * @return the size of the generated byte array for calls to {@link #nextBytes() nextBytes()}. */ @@ -92,7 +93,7 @@ public int getDefaultNextBytesSize() { /** * Sets the size of the generated byte array for calls to {@link #nextBytes() nextBytes()}. Defaults to - * {@code 128}, a commonly used number in cryptographic algorithms. + * {@code 16}, which equals 128 bits, a size commonly used in cryptographic algorithms. * * @param defaultNextBytesSize the size of the generated byte array for calls to {@link #nextBytes() nextBytes()}. * @throws IllegalArgumentException if the argument is 0 or negative diff --git a/core/src/main/java/org/apache/shiro/crypto/hash/AbstractHash.java b/core/src/main/java/org/apache/shiro/crypto/hash/AbstractHash.java index 41b2de8bf7..5ec16da6d3 100644 --- a/core/src/main/java/org/apache/shiro/crypto/hash/AbstractHash.java +++ b/core/src/main/java/org/apache/shiro/crypto/hash/AbstractHash.java @@ -288,7 +288,10 @@ public boolean equals(Object o) { * @return toHex().hashCode() */ public int hashCode() { - return toHex().hashCode(); + if (this.bytes == null || this.bytes.length == 0) { + return 0; + } + return Arrays.hashCode(this.bytes); } private static void printMainUsage(Class clazz, String type) { diff --git a/core/src/main/java/org/apache/shiro/crypto/hash/ConfigurableHasher.java b/core/src/main/java/org/apache/shiro/crypto/hash/ConfigurableHashService.java similarity index 74% rename from core/src/main/java/org/apache/shiro/crypto/hash/ConfigurableHasher.java rename to core/src/main/java/org/apache/shiro/crypto/hash/ConfigurableHashService.java index 2a99a901ef..38c8b469a9 100644 --- a/core/src/main/java/org/apache/shiro/crypto/hash/ConfigurableHasher.java +++ b/core/src/main/java/org/apache/shiro/crypto/hash/ConfigurableHashService.java @@ -19,20 +19,22 @@ package org.apache.shiro.crypto.hash; import org.apache.shiro.crypto.RandomNumberGenerator; +import org.apache.shiro.util.ByteSource; /** - * A {@code Hasher} that allows configuration of its strategy via JavaBeans-compatible setter methods. + * A {@code HashService} that allows configuration of its strategy via JavaBeans-compatible setter methods. * * @since 1.2 */ -public interface ConfigurableHasher extends Hasher { +public interface ConfigurableHashService extends HashService { /** - * Sets the 'private' base salt to be paired with a 'public' (random or supplied) salt during hash computation. + * Sets the 'private' (internal) salt to be paired with a 'public' (random or supplied) salt during hash computation. * - * @param baseSalt the 'private' base salt to be paired with a 'public' (random or supplied) salt during hash computation. + * @param privateSalt the 'private' internal salt to be paired with a 'public' (random or supplied) salt during + * hash computation. */ - void setBaseSalt(byte[] baseSalt); + void setPrivateSalt(ByteSource privateSalt); /** * Sets the number of hash iterations that will be performed during hash computation. @@ -45,8 +47,8 @@ public interface ConfigurableHasher extends Hasher { * Sets the name of the {@link java.security.MessageDigest MessageDigest} algorithm that will be used to compute * hashes. * - * @param name the name of the {@link java.security.MessageDigest MessageDigest} algorithm that will be used to compute - * hashes. + * @param name the name of the {@link java.security.MessageDigest MessageDigest} algorithm that will be used to + * compute hashes. */ void setHashAlgorithmName(String name); diff --git a/core/src/main/java/org/apache/shiro/crypto/hash/DefaultHashService.java b/core/src/main/java/org/apache/shiro/crypto/hash/DefaultHashService.java new file mode 100644 index 0000000000..42ce4ef1fd --- /dev/null +++ b/core/src/main/java/org/apache/shiro/crypto/hash/DefaultHashService.java @@ -0,0 +1,342 @@ +/* + * 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.crypto.hash; + +import org.apache.shiro.crypto.RandomNumberGenerator; +import org.apache.shiro.crypto.SecureRandomNumberGenerator; +import org.apache.shiro.util.ByteSource; + +/** + * Default implementation of the {@link HashService} interface, supporting a customizable hash algorithm name, + * secure-random salt generation, multiple hash iterations and an optional internal + * {@link #setPrivateSalt(ByteSource) privateSalt}. + *

Private Salt

+ * If using this implementation as part of a password hashing strategy, it might be desirable to configure a + * {@link #setPrivateSalt(ByteSource) private salt}: + *

+ * A hash and the salt used to compute it are often stored together. If an attacker is ever able to access + * the hash (e.g. during password cracking) and it has the full salt value, the attacker has all of the input necessary + * to try to brute-force crack the hash (source + complete salt). + *

+ * However, if part of the salt is not available to the attacker (because it is not stored with the hash), it is + * much harder to crack the hash value since the attacker does not have the complete inputs necessary. + *

+ * The {@link #getPrivateSalt() privateSalt} property exists to satisfy this private-and-not-shared part of the salt. + * If you configure this attribute, you can obtain this additional very important safety feature. + *

+ * *By default, the {@link #getPrivateSalt() privateSalt} is null, since a sensible default cannot be used that + * isn't easily compromised (because Shiro is an open-source project and any default could be easily seen and used). + *

Random Salts

+ * When a salt is not specified in a request, this implementation generates secure random salts via its + * {@link #setRandomNumberGenerator(org.apache.shiro.crypto.RandomNumberGenerator) randomNumberGenerator} property. + * Random salts (and potentially combined with the internal {@link #getPrivateSalt() privateSalt}) is a very strong + * salting strategy, as salts should ideally never be based on known/guessable data. The default instance is a + * {@link SecureRandomNumberGenerator}. + *

Password Hash Iterations

+ * Secure hashing strategies often employ multiple hash iterations to slow down the hashing process. This technique + * is usually used for password hashing, since the longer it takes to compute a password hash, the longer it would + * take for an attacker to compromise a password. This + * Katasoft blog article + * explains in greater detail why this is useful, as well as information on how many iterations is 'enough'. + *

+ * You may set the number of hash iterations via the {@link #setHashIterations(int)} property. The default is + * {@code 1}, but should be increased significantly if the {@code HashService} is intended to be used for password + * hashing. See the linked blog article for more info. + *

Hash Algorithm

+ * You may specify a hash algorithm via the {@link #setHashAlgorithmName(String)} property. Any algorithm name + * understood by the JDK + * {@link java.security.MessageDigest#getInstance(String) MessageDigest.getInstance(String algorithmName)} method + * will work. The default is {@code SHA-512}. + * + * @since 1.2 + */ +public class DefaultHashService implements ConfigurableHashService { + + /** + * The RandomNumberGenerator to use to randomly generate the public part of the hash salt. + */ + private RandomNumberGenerator rng; + + /** + * The MessageDigest name of the hash algorithm to use for computing hashes. + */ + private String algorithmName; + + /** + * The 'private' part of the hash salt. + */ + private ByteSource privateSalt; + + /** + * The number of hash iterations to perform when computing hashes. + */ + private int iterations; + + /** + * Whether or not to generate public salts if a request does not provide one. + */ + private boolean generatePublicSalt; + + /** + * Constructs a new {@code DefaultHashService} instance with the following defaults: + * + *

+ * If this hashService will be used for password hashing it is recommended to set the + * {@link #setPrivateSalt(ByteSource) privateSalt} and significantly increase the number of + * {@link #setHashIterations(int) hashIterations}. See the class-level JavaDoc for more information. + */ + public DefaultHashService() { + this.algorithmName = "SHA-512"; + this.iterations = 1; + this.generatePublicSalt = false; + this.rng = new SecureRandomNumberGenerator(); + } + + /** + * Computes and responds with a hash based on the specified request. + *

+ * This implementation functions as follows: + *

+ *

+ * The returned {@code Hash}'s {@link Hash#getSalt() salt} property + * will contain only the 'public' part of the salt and NOT the privateSalt. See the class-level + * JavaDoc explanation for more info. + * + * @param request the request to process + * @return the response containing the result of the hash computation, as well as any hash salt used that should be + * exposed to the caller. + */ + public Hash computeHash(HashRequest request) { + if (request == null) { + return null; + } + ByteSource source = request.getSource(); + + byte[] sourceBytes = source != null ? source.getBytes() : null; + if (sourceBytes == null || sourceBytes.length == 0) { + return null; + } + + ByteSource requestSalt = request.getSalt(); + ByteSource publicSalt = requestSalt != null ? requestSalt : null; + if (requestSalt != null && (requestSalt.getBytes() == null || requestSalt.getBytes().length == 0)) { + publicSalt = null; + } + + if (publicSalt == null) { + publicSalt = getRandomNumberGenerator().nextBytes(); + } + + String algorithmName = getHashAlgorithmName(); + ByteSource privateSalt = getPrivateSalt(); + ByteSource combinedSalt = combine(privateSalt, publicSalt); + int iterations = Math.max(1, getHashIterations()); + + Hash computed = new SimpleHash(algorithmName, sourceBytes, combinedSalt, iterations); + + SimpleHash result = new SimpleHash(algorithmName); + result.setBytes(computed.getBytes()); + result.setIterations(iterations); + //Only expose the public salt - not the real/combined salt that was used: + result.setSalt(publicSalt); + + return result; + } + + + /** + * Returns the public salt that should be used to compute a hash based on the specified request or + * {@code null} if no public salt should be used. + *

+ * This implementation functions as follows: + *

    + *
  1. If the request salt is not null and non-empty, this will be used, return it.
  2. + *
  3. If the request salt is null or empty: + *
      + *
    1. If a private salt has been set OR {@link #isGeneratePublicSalt()} is {@code true}, + * auto generate a random public salt via the configured + * {@link #getRandomNumberGenerator() randomNumberGenerator}.
    2. + *
    3. If a private salt has not been configured and {@link #isGeneratePublicSalt()} is {@code false}, + * do nothing - return {@code null} to indicate a salt should not be used during hash computation.
    4. + *
    + *
  4. + *
+ * + * @param request request the request to process + * @return the public salt that should be used to compute a hash based on the specified request or + * {@code null} if no public salt should be used. + */ + protected ByteSource getPublicSalt(HashRequest request) { + + ByteSource publicSalt = request.getSalt(); + + if (publicSalt != null && !publicSalt.isEmpty()) { + //a public salt was explicitly requested to be used - go ahead and use it: + return publicSalt; + } + + publicSalt = null; + + //check to see if we need to generate one: + ByteSource privateSalt = getPrivateSalt(); + boolean privateSaltExists = privateSalt != null && !privateSalt.isEmpty(); + + //If a private salt exists, we must generate a public salt to protect the integrity of the private salt. + //Or generate it if the instance is explicitly configured to do so: + if (privateSaltExists || isGeneratePublicSalt()) { + publicSalt = getRandomNumberGenerator().nextBytes(); + } + + return publicSalt; + } + + /** + * Combines the specified 'private' salt bytes with the specified additional extra bytes to use as the + * total salt during hash computation. {@code privateSaltBytes} will be {@code null} }if no private salt has been + * configured. + * + * @param privateSalt the (possibly {@code null}) 'private' salt to combine with the specified extra bytes + * @param publicSalt the extra bytes to use in addition to the given private salt. + * @return a combination of the specified private salt bytes and extra bytes that will be used as the total + * salt during hash computation. + */ + protected ByteSource combine(ByteSource privateSalt, ByteSource publicSalt) { + + byte[] privateSaltBytes = privateSalt != null ? privateSalt.getBytes() : null; + int privateSaltLength = privateSaltBytes != null ? privateSaltBytes.length : 0; + + byte[] publicSaltBytes = publicSalt != null ? publicSalt.getBytes() : null; + int extraBytesLength = publicSaltBytes != null ? publicSaltBytes.length : 0; + + int length = privateSaltLength + extraBytesLength; + + if (length <= 0) { + return null; + } + + byte[] combined = new byte[length]; + + int i = 0; + for (int j = 0; j < privateSaltLength; j++) { + assert privateSaltBytes != null; + combined[i++] = privateSaltBytes[j]; + } + for (int j = 0; j < extraBytesLength; j++) { + assert publicSaltBytes != null; + combined[i++] = publicSaltBytes[j]; + } + + return ByteSource.Util.bytes(combined); + } + + public void setHashAlgorithmName(String name) { + this.algorithmName = name; + } + + public String getHashAlgorithmName() { + return this.algorithmName; + } + + public void setPrivateSalt(ByteSource privateSalt) { + this.privateSalt = privateSalt; + } + + public ByteSource getPrivateSalt() { + return this.privateSalt; + } + + public void setHashIterations(int count) { + this.iterations = count; + } + + public int getHashIterations() { + return this.iterations; + } + + public void setRandomNumberGenerator(RandomNumberGenerator rng) { + this.rng = rng; + } + + public RandomNumberGenerator getRandomNumberGenerator() { + return this.rng; + } + + /** + * Returns {@code true} if a public salt should be randomly generated and used to compute a hash if a + * {@link HashRequest} does not specify a salt, {@code false} otherwise. + *

+ * The default value is {@code false} but should definitely be set to {@code true} if the + * {@code HashService} instance is being used for password hashing. + *

+ * NOTE: this property only has an effect if a {@link #getPrivateSalt() privateSalt} is NOT configured. If a + * private salt has been configured and a request does not provide a salt, a random salt will always be generated + * to protect the integrity of the private salt (without a public salt, the private salt would be exposed as-is, + * which is undesirable). + * + * @return {@code true} if a public salt should be randomly generated and used to compute a hash if a + * {@link HashRequest} does not specify a salt, {@code false} otherwise. + */ + public boolean isGeneratePublicSalt() { + return generatePublicSalt; + } + + /** + * Sets whether or not a public salt should be randomly generated and used to compute a hash if a + * {@link HashRequest} does not specify a salt. + *

+ * The default value is {@code false} but should definitely be set to {@code true} if the + * {@code HashService} instance is being used for password hashing. + *

+ * NOTE: this property only has an effect if a {@link #getPrivateSalt() privateSalt} is NOT configured. If a + * private salt has been configured and a request does not provide a salt, a random salt will always be generated + * to protect the integrity of the private salt (without a public salt, the private salt would be exposed as-is, + * which is undesirable). + * + * @param generatePublicSalt whether or not a public salt should be randomly generated and used to compute a hash + * if a {@link HashRequest} does not specify a salt. + */ + public void setGeneratePublicSalt(boolean generatePublicSalt) { + this.generatePublicSalt = generatePublicSalt; + } +} diff --git a/core/src/main/java/org/apache/shiro/crypto/hash/DefaultHasher.java b/core/src/main/java/org/apache/shiro/crypto/hash/DefaultHasher.java deleted file mode 100644 index 19115564e3..0000000000 --- a/core/src/main/java/org/apache/shiro/crypto/hash/DefaultHasher.java +++ /dev/null @@ -1,231 +0,0 @@ -/* - * 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.crypto.hash; - -import org.apache.shiro.crypto.RandomNumberGenerator; -import org.apache.shiro.crypto.SecureRandomNumberGenerator; -import org.apache.shiro.util.ByteSource; - -/** - * Default implementation of the {@link Hasher} interface, supporting secure-random salt generation, an internal - * private {@link #setBaseSalt(byte[]) baseSalt}, multiple hash iterations and customizable hash algorithm name. - *

Base Salt

- * It is strongly recommended to configure a {@link #setBaseSalt(byte[]) base salt}. - * Indeed, the {@link Hasher} concept exists largely to support the {@code base salt} concept: - *

- * A hash and the salt used to compute it are often stored together. If an attacker is ever able to access - * the hash (e.g. during password cracking) and it has the full salt value, the attacker has all of the input necessary - * to try to brute-force crack the hash (source + complete salt). - *

- * However, if part of the salt is not available to the attacker (because it is not stored with the hash), it is - * much harder to crack the hash value since the attacker does not have the complete inputs necessary. - *

- * The {@link #getBaseSalt() baseSalt} property exists to satisfy this private-and-not-shared part of the salt. If you - * configure this attribute, you obtain this additional very important safety feature. - *

- * *By default, the {@link #getBaseSalt() baseSalt} is null, since a sensible default cannot be used that isn't - * easily compromised (because Shiro is an open-source project and any default could be easily seen and used). It is - * expected all end-users will want to provide their own. - *

Random Salts

- * When a salt is not specified in a request, this implementation generates secure random salts via its - * {@link #setRandomNumberGenerator(org.apache.shiro.crypto.RandomNumberGenerator) randomNumberGenerator} property. - * Random salts (combined with the internal {@link #getBaseSalt() baseSalt}) is the strongest salting strategy, - * as salts should ideally never be based on known/guessable data. The default is a - * {@link SecureRandomNumberGenerator}. - *

Password Hash Iterations

- * The most secure hashing strategy employs multiple hash iterations to slow down the hashing process. This technique - * is usually used for password hashing, since the longer it takes to compute a password hash, the longer it would - * take for an attacker to compromise a password. This - * Katasoft blog article - * explains in greater detail why this is useful, as well as information on how many iterations is 'enough'. - *

- * You may set the number of hash iterations via the {@link #setHashIterations(int)} property. The default is - * {@code 1}, but should be increased significantly for password hashing. See the linked blog article for more info. - *

Hash Algorithm

- * You may specify a hash algorithm via the {@link #setHashAlgorithmName(String)} property. The default is - * {@code SHA-512}. - * - * @since 1.2 - */ -public class DefaultHasher implements ConfigurableHasher { - - /** - * The RandomNumberGenerator to use to randomly generate the public part of the hash salt. - */ - private RandomNumberGenerator rng; - - /** - * The MessageDigest name of the hash algorithm to use for computing hashes. - */ - private String algorithmName; - - /** - * The 'private' part of the hash salt. - */ - private byte[] baseSalt; - - /** - * The number of hash iterations to perform when computing hashes. - */ - private int iterations; - - /** - * Constructs a new {@code DefaultHasher} instance with the following defaults: - * - *

- * If this hasher will be used for password hashing it is strongly recommended to set the - * {@link #setBaseSalt(byte[]) baseSalt} and significantly increase the number of - * {@link #setHashIterations(int) hashIterations}. See the class-level JavaDoc for more information. - */ - public DefaultHasher() { - this.algorithmName = "SHA-512"; - this.iterations = 1; - this.rng = new SecureRandomNumberGenerator(); - } - - /** - * Computes and responds with a hash based on the specified request. - *

- * This implementation functions as follows: - *

- *

- * The returned {@code HashResponse}'s {@link org.apache.shiro.crypto.hash.HashResponse#getSalt() salt} property - * will contain only the 'public' part of the salt and NOT the baseSalt. See the class-level JavaDoc - * explanation for more info. - * - * @param request the request to process - * @return the response containing the result of the hash computation, as well as any hash salt used that should be - * exposed to the caller. - */ - public HashResponse computeHash(HashRequest request) { - if (request == null) { - return null; - } - ByteSource source = request.getSource(); - - byte[] sourceBytes = source != null ? source.getBytes() : null; - if (sourceBytes == null || sourceBytes.length == 0) { - return null; - } - - ByteSource requestSalt = request.getSalt(); - byte[] publicSaltBytes = requestSalt != null ? requestSalt.getBytes() : null; - if (publicSaltBytes != null && publicSaltBytes.length == 0) { - publicSaltBytes = null; - } - if (publicSaltBytes == null) { - publicSaltBytes = getRandomNumberGenerator().nextBytes().getBytes(); - } - - String algorithmName = getHashAlgorithmName(); - byte[] baseSalt = getBaseSalt(); - byte[] saltBytes = combine(baseSalt, publicSaltBytes); - int iterations = Math.max(1, getHashIterations()); - - Hash result = new SimpleHash(algorithmName, sourceBytes, saltBytes, iterations); - ByteSource publicSalt = ByteSource.Util.bytes(publicSaltBytes); - - return new SimpleHashResponse(result, publicSalt); - } - - /** - * Combines the specified 'private' base salt bytes with the specified additional extra bytes to use as the - * total salt during hash computation. {@code baseSaltBytes} will be {@code null} }if no base salt has been configured. - * - * @param baseSaltBytes the (possibly {@code null}) 'private' base salt to combine with the specified extra bytes - * @param extraBytes the extra bytes to use in addition to the gien base salt bytes. - * @return a combination of the specified base salt bytes and extra bytes that will be used as the total - * salt during hash computation. - */ - protected byte[] combine(byte[] baseSaltBytes, byte[] extraBytes) { - int baseSaltLength = baseSaltBytes != null ? baseSaltBytes.length : 0; - int randomBytesLength = extraBytes != null ? extraBytes.length : 0; - - int length = baseSaltLength + randomBytesLength; - byte[] combined = new byte[length]; - - int i = 0; - for (int j = 0; j < baseSaltLength; j++) { - assert baseSaltBytes != null; - combined[i++] = baseSaltBytes[j]; - } - for (int j = 0; j < randomBytesLength; j++) { - assert extraBytes != null; - combined[i++] = extraBytes[j]; - } - - return combined; - } - - - public void setHashAlgorithmName(String name) { - this.algorithmName = name; - } - - public String getHashAlgorithmName() { - return this.algorithmName; - } - - public void setBaseSalt(byte[] baseSalt) { - this.baseSalt = baseSalt; - } - - public byte[] getBaseSalt() { - return this.baseSalt; - } - - public void setHashIterations(int count) { - this.iterations = count; - } - - public int getHashIterations() { - return this.iterations; - } - - public void setRandomNumberGenerator(RandomNumberGenerator rng) { - this.rng = rng; - } - - public RandomNumberGenerator getRandomNumberGenerator() { - return this.rng; - } -} diff --git a/core/src/main/java/org/apache/shiro/crypto/hash/Hash.java b/core/src/main/java/org/apache/shiro/crypto/hash/Hash.java index f947ce2957..8760895037 100644 --- a/core/src/main/java/org/apache/shiro/crypto/hash/Hash.java +++ b/core/src/main/java/org/apache/shiro/crypto/hash/Hash.java @@ -26,9 +26,8 @@ * inherited {@link #toHex() toHex()} and {@link #toBase64() toBase64()} methods. *

* The bytes returned by the parent interface's {@link #getBytes() getBytes()} are the hashed value of the - * original input source. + * original input source, also known as the 'checksum' or 'digest'. * - * @see AbstractHash * @see Md2Hash * @see Md5Hash * @see Sha1Hash @@ -48,4 +47,21 @@ public interface Hash extends ByteSource { * @since 1.1 */ String getAlgorithmName(); + + /** + * Returns a salt used to compute the hash or {@code null} if no salt was used. + * + * @return a salt used to compute the hash or {@code null} if no salt was used. + * @since 1.2 + */ + ByteSource getSalt(); + + /** + * Returns the number of hash iterations used to compute the hash. + * + * @return the number of hash iterations used to compute the hash. + * @since 1.2 + */ + int getIterations(); + } diff --git a/core/src/main/java/org/apache/shiro/crypto/hash/HashRequest.java b/core/src/main/java/org/apache/shiro/crypto/hash/HashRequest.java index 6ee8698f0a..82376ed5d3 100644 --- a/core/src/main/java/org/apache/shiro/crypto/hash/HashRequest.java +++ b/core/src/main/java/org/apache/shiro/crypto/hash/HashRequest.java @@ -21,33 +21,205 @@ import org.apache.shiro.util.ByteSource; /** - * A {@code HashRequest} is composed of data that will be used to create a hash by a {@link Hasher}. + * A {@code HashRequest} is composed of data that will be used by a {@link HashService} to compute a hash (aka + * 'digest'). While you can instantiate a concrete {@code HashRequest} class directly, most will find using the + * {@link HashRequest.Builder} more convenient. * - * @see SimpleHashRequest + * @see HashRequest.Builder * @since 1.2 */ public interface HashRequest { /** - * Returns the source data that will be hashed by a {@link Hasher}. + * Returns the source data that will be hashed by a {@link HashService}. For example, this might be a + * {@code ByteSource} representation of a password, or file, etc. * - * @return the source data that will be hashed by a {@link Hasher}. + * @return the source data that will be hashed by a {@link HashService}. */ ByteSource getSource(); /** - * Returns a salt to be used by the {@link Hasher} during hash computation, or {@code null} if no salt is provided - * as part of the request. + * Returns a salt to be used by the {@link HashService} during hash computation, or {@code null} if no salt is + * provided as part of the request. *

- * Note that a {@code null} return value does not necessarily mean a salt won't be used at all - it just - * means that the request didn't include one. The servicing {@link Hasher} is free to provide a salting + * Note that a {@code null} value does not necessarily mean a salt won't be used at all - it just + * means that the request didn't include one. The servicing {@link HashService} is free to provide a salting * strategy for a request, even if the request did not specify one. + * + * @return a salt to be used by the {@link HashService} during hash computation, or {@code null} if no salt is + * provided as part of the request. + */ + ByteSource getSalt(); + + /** + * Returns the number of requested hash iterations to be performed when computing the final {@code Hash} result. + * A non-positive (0 or less) indicates that the {@code HashService}'s default iteration configuration should + * be used. A positive value overrides the {@code HashService}'s configuration for a single request. *

- * NOTE: if + * Note that a {@code HashService} is free to ignore this number if it determines the number is not sufficient + * to meet a desired level of security. * - * @return a salt to be used by the {@link Hasher} during hash computation, or {@code null} if no salt is provided - * as part of the request. + * @return the number of requested hash iterations to be performed when computing the final {@code Hash} result. */ + int getIterations(); - ByteSource getSalt(); + /** + * Returns the name of the hash algorithm the {@code HashService} should use when computing the {@link Hash}, or + * {@code null} if the default algorithm configuration of the {@code HashService} should be used. A non-null value + * overrides the {@code HashService}'s configuration for a single request. + *

+ * Note that a {@code HashService} is free to ignore this value if it determines that the algorithm is not + * sufficient to meet a desired level of security. + * + * @return the name of the hash algorithm the {@code HashService} should use when computing the {@link Hash}, or + * {@code null} if the default algorithm configuration of the {@code HashService} should be used. + */ + String getAlgorithmName(); + + /** + * A Builder class representing the Builder design pattern for constructing {@link HashRequest} instances. + * + * @see SimpleHashRequest + * @since 1.2 + */ + public static class Builder { + + private ByteSource source; + private ByteSource salt; + private int iterations; + private String algorithmName; + + /** + * Default no-arg constructor. + */ + public Builder() { + this.iterations = 0; + } + + /** + * Sets the source data that will be hashed by a {@link HashService}. For example, this might be a + * {@code ByteSource} representation of a password, or file, etc. + * + * @param source the source data that will be hashed by a {@link HashService}. + * @return this {@code Builder} instance for method chaining. + * @see HashRequest#getSource() + * @see #setSource(Object) + */ + public Builder setSource(ByteSource source) { + this.source = source; + return this; + } + + /** + * Sets the source data that will be hashed by a {@link HashService}. + *

+ * This is a convenience alternative to {@link #setSource(ByteSource)}: it will attempt to convert the + * argument into a {@link ByteSource} instance using Shiro's default conversion heuristics + * (as defined by {@link ByteSource.Util#isCompatible(Object) ByteSource.Util.isCompatible}. If the object + * cannot be heuristically converted to a {@code ByteSource}, an {@code IllegalArgumentException} will be + * thrown. + * + * @param source the byte-backed source data that will be hashed by a {@link HashService}. + * @return this {@code Builder} instance for method chaining. + * @throws IllegalArgumentException if the argument cannot be heuristically converted to a {@link ByteSource} + * instance. + * @see HashRequest#getSource() + * @see #setSource(ByteSource) + */ + public Builder setSource(Object source) throws IllegalArgumentException { + this.source = ByteSource.Util.bytes(source); + return this; + } + + /** + * Sets a salt to be used by the {@link HashService} during hash computation. + *

+ * NOTE: not calling this method does not necessarily mean a salt won't be used at all - it just + * means that the request didn't include a salt. The servicing {@link HashService} is free to provide a salting + * strategy for a request, even if the request did not specify one. You can always check the result + * {@code Hash} {@link Hash#getSalt() getSalt()} method to see what the actual + * salt was (if any), which may or may not match this request salt. + * + * @param salt a salt to be used by the {@link HashService} during hash computation + * @return this {@code Builder} instance for method chaining. + * @see HashRequest#getSalt() + */ + public Builder setSalt(ByteSource salt) { + this.salt = salt; + return this; + } + + /** + * Sets a salt to be used by the {@link HashService} during hash computation. + *

+ * This is a convenience alternative to {@link #setSalt(ByteSource)}: it will attempt to convert the + * argument into a {@link ByteSource} instance using Shiro's default conversion heuristics + * (as defined by {@link ByteSource.Util#isCompatible(Object) ByteSource.Util.isCompatible}. If the object + * cannot be heuristically converted to a {@code ByteSource}, an {@code IllegalArgumentException} will be + * thrown. + * + * @param salt a salt to be used by the {@link HashService} during hash computation. + * @return this {@code Builder} instance for method chaining. + * @throws IllegalArgumentException if the argument cannot be heuristically converted to a {@link ByteSource} + * instance. + * @see #setSalt(ByteSource) + * @see HashRequest#getSalt() + */ + public Builder setSalt(Object salt) throws IllegalArgumentException { + this.salt = ByteSource.Util.bytes(salt); + return this; + } + + /** + * Sets the number of requested hash iterations to be performed when computing the final {@code Hash} result. + * Not calling this method or setting a non-positive value (0 or less) indicates that the {@code HashService}'s + * default iteration configuration should be used. A positive value overrides the {@code HashService}'s + * configuration for a single request. + *

+ * Note that a {@code HashService} is free to ignore this number if it determines the number is not sufficient + * to meet a desired level of security. You can always check the result + * {@code Hash} {@link Hash#getIterations() getIterations()} method to see what the actual + * number of iterations was, which may or may not match this request salt. + * + * @param iterations the number of requested hash iterations to be performed when computing the final + * {@code Hash} result. + * @return this {@code Builder} instance for method chaining. + * @see HashRequest#getIterations() + */ + public Builder setIterations(int iterations) { + this.iterations = iterations; + return this; + } + + /** + * Sets the name of the hash algorithm the {@code HashService} should use when computing the {@link Hash}. + * Not calling this method or setting it to {@code null} indicates the the default algorithm configuration of + * the {@code HashService} should be used. A non-null value + * overrides the {@code HashService}'s configuration for a single request. + *

+ * Note that a {@code HashService} is free to ignore this value if it determines that the algorithm is not + * sufficient to meet a desired level of security. You can always check the result + * {@code Hash} {@link Hash#getAlgorithmName() getAlgorithmName()} method to see what the actual + * algorithm was, which may or may not match this request salt. + * + * @param algorithmName the name of the hash algorithm the {@code HashService} should use when computing the + * {@link Hash}, or {@code null} if the default algorithm configuration of the + * {@code HashService} should be used. + * @return this {@code Builder} instance for method chaining. + * @see HashRequest#getAlgorithmName() + */ + public Builder setAlgorithmName(String algorithmName) { + this.algorithmName = algorithmName; + return this; + } + + /** + * Builds a {@link HashRequest} instance reflecting the specified configuration. + * + * @return a {@link HashRequest} instance reflecting the specified configuration. + */ + public HashRequest build() { + return new SimpleHashRequest(this.algorithmName, this.source, this.salt, this.iterations); + } + } } diff --git a/core/src/main/java/org/apache/shiro/crypto/hash/HashResponse.java b/core/src/main/java/org/apache/shiro/crypto/hash/HashResponse.java deleted file mode 100644 index 92bfea1a00..0000000000 --- a/core/src/main/java/org/apache/shiro/crypto/hash/HashResponse.java +++ /dev/null @@ -1,63 +0,0 @@ -/* - * 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.crypto.hash; - -import org.apache.shiro.util.ByteSource; - -/** - * A {@code HashResponse} represents the data returned from a {@link Hasher} after hashing an input source. - *

- * Note that a {@code HashResposne} may not represent identical output compared to using Shiro's {@link Hash} - * implementations directly. See the {@link #getSalt() getHashSalt()} JavaDoc for further explanation. - * - * @since 1.2 - */ -public interface HashResponse { - - /** - * Returns the hashed data returned by the {@link Hasher}. - * - * @return the hashed data returned by the {@link Hasher}. - */ - Hash getHash(); - - /** - * Returns a salt used by the servicing {@link Hasher} when hashing the input source. This same salt must be - * presented back to the {@code Hasher} if hash comparison/verification will be performed (for example, for - * password hash or file checksum comparisons). - *

- * Note that the salt returned from this method MAY NOT be the exact same salt used to compute the - * {@link #getHash() hash}. Such a thing is common when performing password hashes for example: if the - * {@code Hasher} uses internal/private salt data in addition to a specified or random salt, the complete salt - * should not be accessible with the password hash. If it was, brute force attacks could more easily - * compromise passwords. If part of the salt was not accessible to an attacker (because it is not stored with the - * password), brute-force attacks are much harder to execute. - *

- * This scenario emphasizes that any salt returned from this method should be re-supplied to the same {@code Hasher} - * that computed the original hash if performing comparison/verification. The alternative of, say, using a - * Shiro {@link Hash} implementation directly to perform hash comparisons will likely fail. - *

- * In summary, if a {@link Hasher} returns a salt in a response, it is expected that the same salt - * will be provided to the same {@code Hasher} instance. - * - * @return salt a salt used by the {@link Hasher} when hashing the input source. - */ - ByteSource getSalt(); - -} diff --git a/core/src/main/java/org/apache/shiro/crypto/hash/Hasher.java b/core/src/main/java/org/apache/shiro/crypto/hash/HashService.java similarity index 55% rename from core/src/main/java/org/apache/shiro/crypto/hash/Hasher.java rename to core/src/main/java/org/apache/shiro/crypto/hash/HashService.java index 8fe2c557ac..4dc5019b6e 100644 --- a/core/src/main/java/org/apache/shiro/crypto/hash/Hasher.java +++ b/core/src/main/java/org/apache/shiro/crypto/hash/HashService.java @@ -19,9 +19,9 @@ package org.apache.shiro.crypto.hash; /** - * A {@code Hasher} hashes input sources utilizing a particular hashing strategy. + * A {@code HashService} hashes input sources utilizing a particular hashing strategy. *

- * A {@code Hasher} sits at a higher architectural level than Shiro's simple {@link Hash} classes: it allows + * A {@code HashService} sits at a higher architectural level than Shiro's simple {@link Hash} classes: it allows * for salting and iteration-related strategies to be configured and internalized in a * single component that can be re-used in multiple places in the application. *

@@ -32,36 +32,46 @@ * quickly lead to copy-and-paste behavior. For example, consider this logic which might need to repeated in an * application: *

- * byte[] applicationSalt = ...
- * byte[] randomSalt = {@link org.apache.shiro.crypto.RandomNumberGenerator randomNumberGenerator}.nextBytes().getBytes();
- * byte[] combined = combine(applicationSalt, randomSalt);
- * ByteSource hash = Sha512Hash(source, combined, numHashIterations);
- * ByteSource salt = new SimpleByteSource(combined);
- * save(hash, salt);
+ * int numHashIterations = ...
+ * ByteSource privateSalt = ...
+ * ByteSource randomSalt = {@link org.apache.shiro.crypto.RandomNumberGenerator randomNumberGenerator}.nextBytes();
+ * ByteSource combined = combine(privateSalt, randomSalt);
+ * Hash hash = Sha512Hash(source, combined, numHashIterations);
+ * save(hash);
  * 
* In this example, often only the input source will change during runtime, while the hashing strategy (how salts * are generated or acquired, how many hash iterations will be performed, etc) usually remain consistent. A HashService * internalizes this logic so the above becomes simply this: *
- * HashResponse response = hasher.hash(source);
- * save(response.getHash(), response.getSalt());
+ * HashRequest request = new HashRequest.Builder().source(source).build();
+ * Hash result = hashService.hash(request);
+ * save(result);
  * 
* * @since 1.2 */ -public interface Hasher { +public interface HashService { /** * Computes a hash based on the given request. + * + *

Salt Notice

+ * + * If a salt accompanies the return value + * (i.e. returnedHash.{@link org.apache.shiro.crypto.hash.Hash#getSalt() getSalt()} != null), this + * same exact salt MUST be presented back to the {@code HashService} if hash + * comparison/verification will be performed at a later time (for example, for password hash or file checksum + * comparison). *

- * Note that the response data may not be the same as what would have been achieved by using a {@link Hash} - * implementation directly. See the - * {@link org.apache.shiro.crypto.hash.HashResponse#getSalt() HashResponse.getSalt()} JavaDoc for more information. + * For additional security, the {@code HashService}'s internal implementation may use more complex salting + * strategies than what would be achieved by computing a {@code Hash} manually. + *

+ * In summary, if a {@link HashService} returns a salt in a returned Hash, it is expected that the same salt + * will be provided to the same {@code HashService} instance. * * @param request the request to process - * @return the hashed data as a {@code HashResponse} - * @see org.apache.shiro.crypto.hash.HashResponse#getSalt() + * @return the hashed data + * @see Hash#getSalt() */ - HashResponse computeHash(HashRequest request); - + Hash computeHash(HashRequest request); } diff --git a/core/src/main/java/org/apache/shiro/crypto/hash/SimpleHash.java b/core/src/main/java/org/apache/shiro/crypto/hash/SimpleHash.java index 29934d4b6d..7ee3c40929 100644 --- a/core/src/main/java/org/apache/shiro/crypto/hash/SimpleHash.java +++ b/core/src/main/java/org/apache/shiro/crypto/hash/SimpleHash.java @@ -22,6 +22,7 @@ import org.apache.shiro.codec.CodecException; import org.apache.shiro.codec.Hex; import org.apache.shiro.crypto.UnknownAlgorithmException; +import org.apache.shiro.util.ByteSource; import org.apache.shiro.util.StringUtils; import java.security.MessageDigest; @@ -41,6 +42,8 @@ */ public class SimpleHash extends AbstractHash { + private static final int DEFAULT_ITERATIONS = 1; + /** * The {@link java.security.MessageDigest MessageDigest} algorithm name to use when performing the hash. */ @@ -49,7 +52,17 @@ public class SimpleHash extends AbstractHash { /** * The hashed data */ - private byte[] bytes = null; + private byte[] bytes; + + /** + * Supplied salt, if any. + */ + private ByteSource salt; + + /** + * Number of hash iterations to perform. Defaults to 1 in the constructor. + */ + private int iterations; /** * Cached value of the {@link #toHex() toHex()} call so multiple calls won't incur repeated overhead. @@ -78,6 +91,7 @@ public class SimpleHash extends AbstractHash { */ public SimpleHash(String algorithmName) { this.algorithmName = algorithmName; + this.iterations = DEFAULT_ITERATIONS; } /** @@ -99,7 +113,8 @@ public SimpleHash(String algorithmName) { * @throws UnknownAlgorithmException if the {@code algorithmName} is not available. */ public SimpleHash(String algorithmName, Object source) throws CodecException, UnknownAlgorithmException { - this(algorithmName, source, null, 1); + //noinspection NullableProblems + this(algorithmName, source, null, DEFAULT_ITERATIONS); } /** @@ -121,7 +136,7 @@ public SimpleHash(String algorithmName, Object source) throws CodecException, Un * @throws UnknownAlgorithmException if the {@code algorithmName} is not available. */ public SimpleHash(String algorithmName, Object source, Object salt) throws CodecException, UnknownAlgorithmException { - this(algorithmName, source, salt, 1); + this(algorithmName, source, salt, DEFAULT_ITERATIONS); } /** @@ -153,16 +168,65 @@ public SimpleHash(String algorithmName, Object source, Object salt, int hashIter throw new NullPointerException("algorithmName argument cannot be null or empty."); } this.algorithmName = algorithmName; - hash(source, salt, hashIterations); + this.iterations = Math.max(DEFAULT_ITERATIONS, hashIterations); + ByteSource saltBytes = null; + if (salt != null) { + saltBytes = convertSaltToBytes(salt); + this.salt = saltBytes; + } + ByteSource sourceBytes = convertSourceToBytes(source); + hash(sourceBytes, saltBytes, hashIterations); } - private void hash(Object source, Object salt, int hashIterations) throws CodecException, UnknownAlgorithmException { - byte[] sourceBytes = toBytes(source); - byte[] saltBytes = null; - if (salt != null) { - saltBytes = toBytes(salt); + /** + * Acquires the specified {@code source} argument's bytes and returns them in the form of a {@code ByteSource} instance. + *

+ * This implementation merely delegates to the convenience {@link #toByteSource(Object)} method for generic + * conversion. Can be overridden by subclasses for source-specific conversion. + * + * @param source the source object to be hashed. + * @return the source's bytes in the form of a {@code ByteSource} instance. + * @since 1.2 + */ + protected ByteSource convertSourceToBytes(Object source) { + return toByteSource(source); + } + + /** + * Acquires the specified {@code salt} argument's bytes and returns them in the form of a {@code ByteSource} instance. + *

+ * This implementation merely delegates to the convenience {@link #toByteSource(Object)} method for generic + * conversion. Can be overridden by subclasses for salt-specific conversion. + * + * @param salt the salt to be use for the hash. + * @return the salt's bytes in the form of a {@code ByteSource} instance. + * @since 1.2 + */ + protected ByteSource convertSaltToBytes(Object salt) { + return toByteSource(salt); + } + + /** + * Converts a given object into a {@code ByteSource} instance. Assumes the object can be converted to bytes. + * + * @param o the Object to convert into a {@code ByteSource} instance. + * @return the {@code ByteSource} representation of the specified object's bytes. + * @since 1.2 + */ + protected ByteSource toByteSource(Object o) { + if (o == null) { + return null; } - byte[] hashedBytes = hash(sourceBytes, saltBytes, hashIterations); + if (o instanceof ByteSource) { + return (ByteSource) o; + } + byte[] bytes = toBytes(o); + return ByteSource.Util.bytes(bytes); + } + + private void hash(ByteSource source, ByteSource salt, int hashIterations) throws CodecException, UnknownAlgorithmException { + byte[] saltBytes = salt != null ? salt.getBytes() : null; + byte[] hashedBytes = hash(source.getBytes(), saltBytes, hashIterations); setBytes(hashedBytes); } @@ -175,6 +239,14 @@ public String getAlgorithmName() { return this.algorithmName; } + public ByteSource getSalt() { + return this.salt; + } + + public int getIterations() { + return this.iterations; + } + public byte[] getBytes() { return this.bytes; } @@ -193,6 +265,32 @@ public void setBytes(byte[] alreadyHashedBytes) { this.base64Encoded = null; } + /** + * Sets the iterations used to previously compute AN ALREADY GENERATED HASH. + *

+ * This is provided ONLY to reconstitute an already-created Hash instance. It should ONLY ever be + * invoked when re-constructing a hash instance from an already-hashed value. + * + * @param iterations the number of hash iterations used to previously create the hash/digest. + * @since 1.2 + */ + public void setIterations(int iterations) { + this.iterations = Math.max(DEFAULT_ITERATIONS, iterations); + } + + /** + * Sets the salt used to previously compute AN ALREADY GENERATED HASH. + *

+ * This is provided ONLY to reconstitute a Hash instance that has already been computed. It should ONLY + * ever be invoked when re-constructing a hash instance from an already-hashed value. + * + * @param salt the salt used to previously create the hash/digest. + * @since 1.2 + */ + public void setSalt(ByteSource salt) { + this.salt = salt; + } + /** * Returns the JDK MessageDigest instance to use for executing the hash. * @@ -217,7 +315,7 @@ protected MessageDigest getDigest(String algorithmName) throws UnknownAlgorithmE * @throws UnknownAlgorithmException if the configured {@link #getAlgorithmName() algorithmName} is not available. */ protected byte[] hash(byte[] bytes) throws UnknownAlgorithmException { - return hash(bytes, null, 1); + return hash(bytes, null, DEFAULT_ITERATIONS); } /** @@ -229,7 +327,7 @@ protected byte[] hash(byte[] bytes) throws UnknownAlgorithmException { * @throws UnknownAlgorithmException if the configured {@link #getAlgorithmName() algorithmName} is not available. */ protected byte[] hash(byte[] bytes, byte[] salt) throws UnknownAlgorithmException { - return hash(bytes, salt, 1); + return hash(bytes, salt, DEFAULT_ITERATIONS); } /** @@ -248,7 +346,7 @@ protected byte[] hash(byte[] bytes, byte[] salt, int hashIterations) throws Unkn digest.update(salt); } byte[] hashed = digest.digest(bytes); - int iterations = hashIterations - 1; //already hashed once above + int iterations = hashIterations - DEFAULT_ITERATIONS; //already hashed once above //iterate remaining number: for (int i = 0; i < iterations; i++) { digest.reset(); @@ -257,6 +355,10 @@ protected byte[] hash(byte[] bytes, byte[] salt, int hashIterations) throws Unkn return hashed; } + public boolean isEmpty() { + return this.bytes == null || this.bytes.length == 0; + } + /** * Returns a hex-encoded string of the underlying {@link #getBytes byte array}. *

@@ -321,71 +423,9 @@ public boolean equals(Object o) { * @return toHex().hashCode() */ public int hashCode() { - return toHex().hashCode(); - } - - private static void printMainUsage(Class clazz, String type) { - System.out.println("Prints an " + type + " hash value."); - System.out.println("Usage: java " + clazz.getName() + " [-base64] [-salt ] [-times ] "); - System.out.println("Options:"); - System.out.println("\t-base64\t\tPrints the hash value as a base64 String instead of the default hex."); - System.out.println("\t-salt\t\tSalts the hash with the specified "); - System.out.println("\t-times\t\tHashes the input number of times"); - } - - private static boolean isReserved(String arg) { - return "-base64".equals(arg) || "-times".equals(arg) || "-salt".equals(arg); - } - - static int doMain(Class clazz, String[] args) { - String simple = clazz.getSimpleName(); - int index = simple.indexOf("Hash"); - String type = simple.substring(0, index).toUpperCase(); - - if (args == null || args.length < 1 || args.length > 7) { - printMainUsage(clazz, type); - return -1; - } - boolean hex = true; - String salt = null; - int times = 1; - String text = args[args.length - 1]; - for (int i = 0; i < args.length; i++) { - String arg = args[i]; - if (arg.equals("-base64")) { - hex = false; - } else if (arg.equals("-salt")) { - if ((i + 1) >= (args.length - 1)) { - String msg = "Salt argument must be followed by a salt value. The final argument is " + - "reserved for the value to hash."; - System.out.println(msg); - printMainUsage(clazz, type); - return -1; - } - salt = args[i + 1]; - } else if (arg.equals("-times")) { - if ((i + 1) >= (args.length - 1)) { - String msg = "Times argument must be followed by an integer value. The final argument is " + - "reserved for the value to hash"; - System.out.println(msg); - printMainUsage(clazz, type); - return -1; - } - try { - times = Integer.valueOf(args[i + 1]); - } catch (NumberFormatException e) { - String msg = "Times argument must be followed by an integer value."; - System.out.println(msg); - printMainUsage(clazz, type); - return -1; - } - } + if (this.bytes == null || this.bytes.length == 0) { + return 0; } - - Hash hash = new Md2Hash(text, salt, times); - String hashed = hex ? hash.toHex() : hash.toBase64(); - System.out.print(hex ? "Hex: " : "Base64: "); - System.out.println(hashed); - return 0; + return Arrays.hashCode(this.bytes); } } diff --git a/core/src/main/java/org/apache/shiro/crypto/hash/SimpleHashRequest.java b/core/src/main/java/org/apache/shiro/crypto/hash/SimpleHashRequest.java index 0c7b5a748b..0e528a6409 100644 --- a/core/src/main/java/org/apache/shiro/crypto/hash/SimpleHashRequest.java +++ b/core/src/main/java/org/apache/shiro/crypto/hash/SimpleHashRequest.java @@ -21,39 +21,39 @@ import org.apache.shiro.util.ByteSource; /** - * Simple implementation of {@link HashRequest} that retains the {@link #getSource source} and - * {@link #getSalt salt} properties as private attributes. + * Simple implementation of {@link HashRequest} that can be used when interacting with a {@link HashService}. * * @since 1.2 */ public class SimpleHashRequest implements HashRequest { - private final ByteSource source; - private final ByteSource salt; + private final ByteSource source; //cannot be null - this is the source to hash. + private final ByteSource salt; //null = no salt specified + private final int iterations; //0 = not specified by the requestor; let the HashService decide. + private final String algorithmName; //null = let the HashService decide. /** - * Creates a new {@code SimpleHashRequest} with the specified source to be hashed. + * Creates a new SimpleHashRequest instance. * - * @param source the source data to be hashed - * @throws NullPointerException if the specified {@code source} argument is {@code null}. - */ - public SimpleHashRequest(ByteSource source) throws NullPointerException { - this(source, null); - } - - /** - * Creates a new {@code SimpleHashRequest} with the specified source and salt. + * @param algorithmName the name of the hash algorithm to use. This is often null as the + * {@link HashService} implementation is usually configured with an appropriate algorithm name, but this + * can be non-null if the hash service's algorithm should be overridden with a specific one for the duration + * of the request. * - * @param source the source data to be hashed - * @param salt a salt a salt to be used by the {@link Hasher} during hash computation. - * @throws NullPointerException if the specified {@code source} argument is {@code null}. + * @param source the source to be hashed + * @param salt any public salt which should be used when computing the hash + * @param iterations the number of hash iterations to execute. Zero (0) indicates no iterations were specified + * for the request, at which point the number of iterations is decided by the {@code HashService} + * @throws NullPointerException if {@code source} is null or empty. */ - public SimpleHashRequest(ByteSource source, ByteSource salt) throws NullPointerException { - this.source = source; - this.salt = salt; + public SimpleHashRequest(String algorithmName, ByteSource source, ByteSource salt, int iterations) { if (source == null) { - throw new NullPointerException("source argument cannot be null."); + throw new NullPointerException("source argument cannot be null"); } + this.source = source; + this.salt = salt; + this.algorithmName = algorithmName; + this.iterations = Math.max(0, iterations); } public ByteSource getSource() { @@ -63,4 +63,12 @@ public ByteSource getSource() { public ByteSource getSalt() { return this.salt; } + + public int getIterations() { + return iterations; + } + + public String getAlgorithmName() { + return algorithmName; + } } diff --git a/core/src/main/java/org/apache/shiro/crypto/hash/format/Base64Format.java b/core/src/main/java/org/apache/shiro/crypto/hash/format/Base64Format.java new file mode 100644 index 0000000000..78742c0c5c --- /dev/null +++ b/core/src/main/java/org/apache/shiro/crypto/hash/format/Base64Format.java @@ -0,0 +1,41 @@ +/* + * 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.crypto.hash.format; + +import org.apache.shiro.crypto.hash.Hash; + +/** + * {@code HashFormat} that outputs only the hash's digest bytes in Base64 format. It does not print out + * anything else (salt, iterations, etc). This implementation is mostly provided as a convenience for + * command-line hashing. + * + * @since 1.2 + */ +public class Base64Format implements HashFormat { + + /** + * Returns {@code hash != null ? hash.toBase64() : null}. + * + * @param hash the hash instance to format into a String. + * @return {@code hash != null ? hash.toBase64() : null}. + */ + public String format(Hash hash) { + return hash != null ? hash.toBase64() : null; + } +} diff --git a/core/src/main/java/org/apache/shiro/crypto/hash/format/DefaultHashFormatFactory.java b/core/src/main/java/org/apache/shiro/crypto/hash/format/DefaultHashFormatFactory.java new file mode 100644 index 0000000000..e6557fe78e --- /dev/null +++ b/core/src/main/java/org/apache/shiro/crypto/hash/format/DefaultHashFormatFactory.java @@ -0,0 +1,307 @@ +/* + * 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.crypto.hash.format; + +import org.apache.shiro.util.ClassUtils; +import org.apache.shiro.util.CollectionUtils; +import org.apache.shiro.util.StringUtils; +import org.apache.shiro.util.UnknownClassException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +/** + * This default {@code HashFormatFactory} implementation heuristically determines a {@code HashFormat} class to + * instantiate based on the input argument and returns a new instance of the discovered class. The heuristics are + * detailed in the {@link #getInstance(String) getInstance} method documentation.j + * + * @since 1.2 + */ +public class DefaultHashFormatFactory implements HashFormatFactory { + + private static final Logger log = LoggerFactory.getLogger(DefaultHashFormatFactory.class); + + private static final String DEFAULT_HASH_FORMAT_PACKAGE_NAME = HashFormat.class.getPackage().getName(); + + private Map formatClassNames; //id - to - fully qualified class name + + private Set searchPackages; + + public DefaultHashFormatFactory() { + this.searchPackages = new HashSet(); + formatClassNames = new HashMap(); + formatClassNames.put(Shiro1CryptFormat.ID, Shiro1CryptFormat.class.getName()); + } + + public Map getFormatClassNames() { + return formatClassNames; + } + + public void setFormatClassNames(Map formatClassNames) { + this.formatClassNames = formatClassNames; + } + + public Set getSearchPackages() { + return searchPackages; + } + + public void setSearchPackages(Set searchPackages) { + this.searchPackages = searchPackages; + } + + public HashFormat getInstance(String in) { + if (in == null) { + return null; + } + + HashFormat hashFormat = null; + + Class clazz = getHashFormatClass(in); + + //The 'in' argument didn't result in a corresponding HashFormat class using our heuristics. + //As a fallback, check to see if the argument is an MCF-formatted string. If it is, odds are very high + //that the MCF ID id is the lookup token we can use to find a corresponding HashFormat class: + if (clazz == null && in.startsWith(ModularCryptFormat.TOKEN_DELIMITER)) { + String test = in.substring(ModularCryptFormat.TOKEN_DELIMITER.length()); + String[] tokens = test.split("\\" + ModularCryptFormat.TOKEN_DELIMITER); + //the MCF ID is always the first token in the delimited string: + String possibleMcfId = (tokens != null && tokens.length > 0) ? tokens[0] : null; + if (possibleMcfId != null) { + //found a possible MCF ID - test it using our heuristics to see if we can find a corresponding class: + clazz = getHashFormatClass(possibleMcfId); + } + } + + if (clazz != null) { + //we found a HashFormat class - instantiate it: + hashFormat = newHashFormatInstance(clazz); + + //do further compatibility testing if we can: + if (hashFormat instanceof ParsableHashFormat) { + //This is not really an efficient way to test for format compatibility, but + //there is no other way that guarantees compatibility that I can think of at the moment. + //perhaps an isCompatible method can be introduced? The struggle I have with this is how do you + //determine compatibility without parsing it fully? If not fully parsed, then it truly can't be + //guaranteed compatible - at which point, you might as well just parse the thing - L.H. 22 Nov 2011 + try { + ParsableHashFormat phf = (ParsableHashFormat)hashFormat; + phf.parse(in); + // no exception - must be a match: + return phf; + } catch (RuntimeException re) { + log.debug("Candidate format instance of type [{}] is unable to " + + "parse formatted String [{}]. Ignoring.", clazz, in); + log.trace("HashFormat parsing caused exception: ", re); + } + } + } + + return hashFormat; + } + + /** + * Heuristically determine the fully qualified HashFormat implementation class name based on the specified + * token. + *

+ * This implementation functions as follows: + *

+ * All configured {@link #getSearchPackages() searchPackages} will be searched using heuristics defined in the + * {@link #getHashFormatClass(String, String) getHashFormatClass(packageName, token)} method documentation (relaying + * the {@code token} argument to that method for each configured package). + *

+ * If the class was not found in any configured {@code searchPackages}, the default + * {@code org.apache.shiro.crypto.hash.format} package will be attempted as a final fallback. + *

+ * If the class was not discovered in any of the {@code searchPackages} or in Shiro's default fallback package, + * {@code null} is returned to indicate the class could not be found. + * + * @param token the string token from which a class name will be heuristically determined. + * @return the discovered HashFormat class implementation or {@code null} if no class could be heuristically determined. + */ + protected Class getHashFormatClass(String token) { + + //check to see if the token is a fully qualified class name: + Class clazz = lookupHashFormatClass(token); + + if (clazz == null) { + //check to see if the token is a FQCN alias: + if (!CollectionUtils.isEmpty(this.formatClassNames)) { + String value = this.formatClassNames.get(token); + if (value != null) { + //found an alias - see if the value is a class: + clazz = lookupHashFormatClass(token); + } + } + } + + if (clazz == null) { + //token wasn't a FQCN or a FQCN alias - try searching in configured packages: + if (!CollectionUtils.isEmpty(this.searchPackages)) { + for (String packageName : this.searchPackages) { + clazz = getHashFormatClass(packageName, token); + if (clazz != null) { + //found it: + break; + } + } + } + } + + if (clazz == null) { + //couldn't find it in any configured search packages. Try Shiro's default search package: + clazz = getHashFormatClass(DEFAULT_HASH_FORMAT_PACKAGE_NAME, token); + } + + if (clazz != null) { + assertHashFormatImpl(clazz); + } + + return clazz; + } + + /** + * Heuristically determine the fully qualified {@code HashFormat} implementation class name in the specified + * package based on the provided token. + *

+ * The token is expected to be a relevant fragment of an unqualified class name in the specified package. + * A 'relevant fragment' can be one of the following: + *

+ *

+ * Some examples: + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + *
Package NameTokenExpected Output ClassNotes
{@code com.foo.whatever}{@code MyBarFormat}{@code com.foo.whatever.MyBarFormat}Token is a complete unqualified class name
{@code com.foo.whatever}{@code Bar}{@code com.foo.whatever.BarFormat} or {@code com.foo.whatever.BarHashFormat} or + * {@code com.foo.whatever.BarCryptFormat}The token is only part of the unqualified class name - i.e. all characters in front of the {@code *Format} + * {@code *HashFormat} or {@code *CryptFormat} suffix. Note that the {@code *Format} variant will be tried before + * {@code *HashFormat} and then finally {@code *CryptFormat}
{@code com.foo.whatever}{@code bar}{@code com.foo.whatever.BarFormat} or {@code com.foo.whatever.BarHashFormat} or + * {@code com.foo.whatever.BarCryptFormat}Exact same output as the above {@code Bar} input example. (The token differs only by the first character)
+ * + * @param packageName the package to search for matching {@code HashFormat} implementations. + * @param token the string token from which a class name will be heuristically determined. + * @return the discovered HashFormat class implementation or {@code null} if no class could be heuristically determined. + */ + protected Class getHashFormatClass(String packageName, String token) { + String test = token; + Class clazz = null; + String pkg = packageName == null ? "" : packageName; + + //1. Assume the arg is a fully qualified class name in the classpath: + clazz = lookupHashFormatClass(test); + + if (clazz == null) { + test = pkg + "." + token; + clazz = lookupHashFormatClass(test); + } + + if (clazz == null) { + test = pkg + "." + StringUtils.uppercaseFirstChar(token) + "Format"; + clazz = lookupHashFormatClass(test); + } + + if (clazz == null) { + test = pkg + "." + token + "Format"; + clazz = lookupHashFormatClass(test); + } + + if (clazz == null) { + test = pkg + "." + StringUtils.uppercaseFirstChar(token) + "HashFormat"; + clazz = lookupHashFormatClass(test); + } + + if (clazz == null) { + test = pkg + "." + token + "HashFormat"; + clazz = lookupHashFormatClass(test); + } + + if (clazz == null) { + test = pkg + "." + StringUtils.uppercaseFirstChar(token) + "CryptFormat"; + clazz = lookupHashFormatClass(test); + } + + if (clazz == null) { + test = pkg + "." + token + "CryptFormat"; + clazz = lookupHashFormatClass(test); + } + + if (clazz == null) { + return null; //ran out of options + } + + assertHashFormatImpl(clazz); + + return clazz; + } + + protected Class lookupHashFormatClass(String name) { + try { + return ClassUtils.forName(name); + } catch (UnknownClassException ignored) { + } + + return null; + } + + protected final void assertHashFormatImpl(Class clazz) { + if (!HashFormat.class.isAssignableFrom(clazz) || clazz.isInterface()) { + throw new IllegalArgumentException("Discovered class [" + clazz.getName() + "] is not a " + + HashFormat.class.getName() + " implementation."); + } + + } + + protected final HashFormat newHashFormatInstance(Class clazz) { + assertHashFormatImpl(clazz); + return (HashFormat)ClassUtils.newInstance(clazz); + } +} diff --git a/core/src/main/java/org/apache/shiro/crypto/hash/format/HashFormat.java b/core/src/main/java/org/apache/shiro/crypto/hash/format/HashFormat.java new file mode 100644 index 0000000000..c65ae78b5f --- /dev/null +++ b/core/src/main/java/org/apache/shiro/crypto/hash/format/HashFormat.java @@ -0,0 +1,45 @@ +/* + * 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.crypto.hash.format; + +import org.apache.shiro.crypto.hash.Hash; + +/** + * A {@code HashFormat} is able to format a {@link Hash} instance into a well-defined formatted String. + *

+ * Note that not all HashFormat algorithms are reversible. That is, they can't be parsed and reconstituted to the + * original Hash instance. The traditional + * Unix crypt(3) is one such format. + *

+ * The formats that are reversible however will be represented as {@link ParsableHashFormat} instances. + * + * @see ParsableHashFormat + * + * @since 1.2 + */ +public interface HashFormat { + + /** + * Returns a formatted string representing the specified Hash instance. + * + * @param hash the hash instance to format into a String. + * @return a formatted string representing the specified Hash instance. + */ + String format(Hash hash); +} diff --git a/core/src/main/java/org/apache/shiro/crypto/hash/format/HashFormatFactory.java b/core/src/main/java/org/apache/shiro/crypto/hash/format/HashFormatFactory.java new file mode 100644 index 0000000000..9a3aa396d3 --- /dev/null +++ b/core/src/main/java/org/apache/shiro/crypto/hash/format/HashFormatFactory.java @@ -0,0 +1,27 @@ +/* + * 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.crypto.hash.format; + +/** + * @since 1.2 + */ +public interface HashFormatFactory { + + HashFormat getInstance(String id); +} diff --git a/core/src/main/java/org/apache/shiro/crypto/hash/format/HexFormat.java b/core/src/main/java/org/apache/shiro/crypto/hash/format/HexFormat.java new file mode 100644 index 0000000000..5730ac990c --- /dev/null +++ b/core/src/main/java/org/apache/shiro/crypto/hash/format/HexFormat.java @@ -0,0 +1,41 @@ +/* + * 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.crypto.hash.format; + +import org.apache.shiro.crypto.hash.Hash; + +/** + * {@code HashFormat} that outputs only The hash's digest bytes in hex format. It does not print out + * anything else (salt, iterations, etc). This implementation is mostly provided as a convenience for + * command-line hashing. + * + * @since 1.2 + */ +public class HexFormat implements HashFormat { + + /** + * Returns {@code hash != null ? hash.toHex() : null}. + * + * @param hash the hash instance to format into a String. + * @return {@code hash != null ? hash.toHex() : null}. + */ + public String format(Hash hash) { + return hash != null ? hash.toHex() : null; + } +} diff --git a/core/src/main/java/org/apache/shiro/crypto/hash/format/ModularCryptFormat.java b/core/src/main/java/org/apache/shiro/crypto/hash/format/ModularCryptFormat.java new file mode 100644 index 0000000000..ce4917556e --- /dev/null +++ b/core/src/main/java/org/apache/shiro/crypto/hash/format/ModularCryptFormat.java @@ -0,0 +1,42 @@ +/* + * 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.crypto.hash.format; + +/** + * A {@code HashFormat} that supports + * Modular Crypt Format token rules. + * + * @see Crypt (unix) + * @see MCF Journal Entry + * @since 1.2 + */ +public interface ModularCryptFormat extends HashFormat { + + public static final String TOKEN_DELIMITER = "$"; + + /** + * Returns the Modular Crypt Format identifier that indicates how the formatted String should be parsed. This id + * is always in the MCF-formatted string's first token. + *

+ * Example values are {@code md5}, {@code 1}, {@code 2}, {@code apr1}, etc. + * + * @return the Modular Crypt Format identifier that indicates how the formatted String should be parsed. + */ + String getId(); +} diff --git a/core/src/main/java/org/apache/shiro/crypto/hash/format/ParsableHashFormat.java b/core/src/main/java/org/apache/shiro/crypto/hash/format/ParsableHashFormat.java new file mode 100644 index 0000000000..045756805b --- /dev/null +++ b/core/src/main/java/org/apache/shiro/crypto/hash/format/ParsableHashFormat.java @@ -0,0 +1,43 @@ +/* + * 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.crypto.hash.format; + +import org.apache.shiro.crypto.hash.Hash; + +/** + * A {@code ParsableHashFormat} is able to parse a formatted string and convert it into a {@link Hash} instance. + *

+ * This interface exists to represent {@code HashFormat}s that can offer two-way conversion + * (Hash -> String, String -> Hash) capabilities. Some HashFormats, such as many {@link ModularCryptFormat}s + * (like Unix Crypt(3)) only support one way conversion and therefore wouldn't implement this interface. + * + * @see Shiro1CryptFormat + * + * @since 1.2 + */ +public interface ParsableHashFormat extends HashFormat { + + /** + * Parses the specified formatted string and returns the corresponding Hash instance. + * + * @param formatted the formatted string representing a Hash. + * @return the corresponding Hash instance. + */ + Hash parse(String formatted); +} diff --git a/core/src/main/java/org/apache/shiro/crypto/hash/format/Shiro1CryptFormat.java b/core/src/main/java/org/apache/shiro/crypto/hash/format/Shiro1CryptFormat.java new file mode 100644 index 0000000000..5f78828731 --- /dev/null +++ b/core/src/main/java/org/apache/shiro/crypto/hash/format/Shiro1CryptFormat.java @@ -0,0 +1,166 @@ +/* + * 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.crypto.hash.format; + +import org.apache.shiro.codec.Base64; +import org.apache.shiro.crypto.hash.Hash; +import org.apache.shiro.crypto.hash.SimpleHash; +import org.apache.shiro.util.ByteSource; +import org.apache.shiro.util.StringUtils; + +/** + * The {@code Shiro1CryptFormat} is a fully reversible + * Modular Crypt Format (MCF). Because it is + * fully reversible (i.e. Hash -> String, String -> Hash), it does NOT use the traditional MCF encoding alphabet + * (the traditional MCF encoding, aka H64, is bit-destructive and cannot be reversed). Instead, it uses fully + * reversible Base64 encoding for the Hash digest and any salt value. + *

Format

+ *

Hash instances formatted with this implementation will result in a String with the following dollar-sign ($) + * delimited format:

+ *
+ * $mcfFormatId$algorithmName$iterationCount$base64EncodedSalt$base64EncodedDigest
+ * 
+ *

Each token is defined as follows:

+ * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + *
PositionTokenDescriptionRequired?
1{@code mcfFormatId}The Modular Crypt Format identifier for this implementation, equal to {@code shiro1}. + * ( This implies that all {@code shiro1} MCF-formatted strings will always begin with the prefix + * {@code $shiro1$} ).true
2{@code algorithmName}The name of the hash algorithm used to perform the hash. This is an algorithm name understood by + * {@code MessageDigest}.{@link java.security.MessageDigest#getInstance(String) getInstance}, for example + * {@code MD5}, {@code SHA-256}, {@code SHA-256}, etc.true
3{@code iterationCount}The number of hash iterations performed.true (1 <= N <= Integer.MAX_VALUE)
4{@code base64EncodedSalt}The Base64-encoded salt byte array. This token only exists if a salt was used to perform the hash.false
5{@code base64EncodedDigest}The Base64-encoded digest byte array. This is the actual hash result.true
+ * + * @see ModularCryptFormat + * @see ParsableHashFormat + * + * @since 1.2 + */ +public class Shiro1CryptFormat implements ModularCryptFormat, ParsableHashFormat { + + public static final String ID = "shiro1"; + public static final String MCF_PREFIX = TOKEN_DELIMITER + ID + TOKEN_DELIMITER; + + public Shiro1CryptFormat() { + } + + public String getId() { + return ID; + } + + public String format(Hash hash) { + if (hash == null) { + return null; + } + + String algorithmName = hash.getAlgorithmName(); + ByteSource salt = hash.getSalt(); + int iterations = hash.getIterations(); + StringBuilder sb = new StringBuilder(MCF_PREFIX).append(algorithmName).append(TOKEN_DELIMITER).append(iterations).append(TOKEN_DELIMITER); + + if (salt != null) { + sb.append(salt.toBase64()); + } + + sb.append(TOKEN_DELIMITER); + sb.append(hash.toBase64()); + + return sb.toString(); + } + + public Hash parse(String formatted) { + if (formatted == null) { + return null; + } + if (!formatted.startsWith(MCF_PREFIX)) { + //TODO create a HashFormatException class + String msg = "The argument is not a valid '" + ID + "' formatted hash."; + throw new IllegalArgumentException(msg); + } + + String suffix = formatted.substring(MCF_PREFIX.length()); + String[] parts = suffix.split("\\$"); + + //last part is always the digest/checksum, Base64-encoded: + int i = parts.length-1; + String digestBase64 = parts[i--]; + //second-to-last part is always the salt, Base64-encoded: + String saltBase64 = parts[i--]; + String iterationsString = parts[i--]; + String algorithmName = parts[i]; + + byte[] digest = Base64.decode(digestBase64); + ByteSource salt = null; + + if (StringUtils.hasLength(saltBase64)) { + byte[] saltBytes = Base64.decode(saltBase64); + salt = ByteSource.Util.bytes(saltBytes); + } + + int iterations; + try { + iterations = Integer.parseInt(iterationsString); + } catch (NumberFormatException e) { + String msg = "Unable to parse formatted hash string: " + formatted; + throw new IllegalArgumentException(msg, e); + } + + SimpleHash hash = new SimpleHash(algorithmName); + hash.setBytes(digest); + if (salt != null) { + hash.setSalt(salt); + } + hash.setIterations(iterations); + + return hash; + } +} diff --git a/core/src/main/java/org/apache/shiro/env/DefaultEnvironment.java b/core/src/main/java/org/apache/shiro/env/DefaultEnvironment.java index 12f06a6c3d..13843ed3e3 100644 --- a/core/src/main/java/org/apache/shiro/env/DefaultEnvironment.java +++ b/core/src/main/java/org/apache/shiro/env/DefaultEnvironment.java @@ -31,7 +31,7 @@ * * @since 1.2 */ -public class DefaultEnvironment implements Environment, Destroyable { +public class DefaultEnvironment implements NamedObjectEnvironment, Destroyable { /** * The default name under which the application's {@code SecurityManager} instance may be acquired, equal to @@ -54,12 +54,12 @@ public DefaultEnvironment() { * * @param seed backing map to use to maintain Shiro objects. */ + @SuppressWarnings({"unchecked"}) public DefaultEnvironment(Map seed) { this.securityManagerName = DEFAULT_SECURITY_MANAGER_KEY; if (seed == null) { throw new IllegalArgumentException("Backing map cannot be null."); } - //noinspection unchecked this.objects = (Map) seed; } @@ -88,8 +88,8 @@ public void setSecurityManager(SecurityManager securityManager) { if (securityManager == null) { throw new IllegalArgumentException("Null SecurityManager instances are not allowed."); } - String key = getSecurityManagerName(); - this.objects.put(key, securityManager); + String name = getSecurityManagerName(); + setObject(name, securityManager); } /** @@ -98,8 +98,8 @@ public void setSecurityManager(SecurityManager securityManager) { * @return the {@code SecurityManager} in the backing map, or {@code null} if it has not yet been populated. */ protected SecurityManager lookupSecurityManager() { - String key = getSecurityManagerName(); - return (SecurityManager) this.objects.get(key); + String name = getSecurityManagerName(); + return getObject(name, SecurityManager.class); } /** @@ -133,6 +133,37 @@ public Map getObjects() { return this.objects; } + @SuppressWarnings({"unchecked"}) + public T getObject(String name, Class requiredType) throws RequiredTypeException { + if (name == null) { + throw new NullPointerException("name parameter cannot be null."); + } + if (requiredType == null) { + throw new NullPointerException("requiredType parameter cannot be null."); + } + Object o = this.objects.get(name); + if (o == null) { + return null; + } + if (!requiredType.isInstance(o)) { + String msg = "Object named '" + name + "' is not of required type [" + requiredType.getName() + "]."; + throw new RequiredTypeException(msg); + } + return (T)o; + } + + public void setObject(String name, Object instance) { + if (name == null) { + throw new NullPointerException("name parameter cannot be null."); + } + if (instance == null) { + this.objects.remove(name); + } else { + this.objects.put(name, instance); + } + } + + public void destroy() throws Exception { LifecycleUtils.destroy(this.objects.values()); } diff --git a/core/src/main/java/org/apache/shiro/env/EnvironmentException.java b/core/src/main/java/org/apache/shiro/env/EnvironmentException.java new file mode 100644 index 0000000000..59c1cbaacb --- /dev/null +++ b/core/src/main/java/org/apache/shiro/env/EnvironmentException.java @@ -0,0 +1,19 @@ +package org.apache.shiro.env; + +import org.apache.shiro.ShiroException; + +/** + * Exception thrown for errors related to {@link Environment} instances or configuration. + * + * @since 1.2 + */ +public class EnvironmentException extends ShiroException { + + public EnvironmentException(String message) { + super(message); + } + + public EnvironmentException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/core/src/main/java/org/apache/shiro/env/NamedObjectEnvironment.java b/core/src/main/java/org/apache/shiro/env/NamedObjectEnvironment.java new file mode 100644 index 0000000000..6d9e0aacfb --- /dev/null +++ b/core/src/main/java/org/apache/shiro/env/NamedObjectEnvironment.java @@ -0,0 +1,22 @@ +package org.apache.shiro.env; + +/** + * An environment that supports object lookup by name. + * + * @since 1.2 + */ +public interface NamedObjectEnvironment extends Environment { + + /** + * Returns the object in Shiro's environment with the specified name and type or {@code null} if + * no object with that name was found. + * + * @param name the assigned name of the object. + * @param requiredType the class that the discovered object should be. If the object is not the specified type, a + * @param the type of the class + * @throws RequiredTypeException if the discovered object does not equal, extend, or implement the specified class. + * @return the object in Shiro's environment with the specified name (of the specified type) or {@code null} if + * no object with that name was found. + */ + T getObject(String name, Class requiredType) throws RequiredTypeException; +} diff --git a/core/src/main/java/org/apache/shiro/env/RequiredTypeException.java b/core/src/main/java/org/apache/shiro/env/RequiredTypeException.java new file mode 100644 index 0000000000..14d13b0613 --- /dev/null +++ b/core/src/main/java/org/apache/shiro/env/RequiredTypeException.java @@ -0,0 +1,18 @@ +package org.apache.shiro.env; + +/** + * Exception thrown when attempting to acquire an object of a required type and that object does not equal, extend, or + * implement a specified {@code Class}. + * + * @since 1.2 + */ +public class RequiredTypeException extends EnvironmentException { + + public RequiredTypeException(String message) { + super(message); + } + + public RequiredTypeException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/core/src/main/java/org/apache/shiro/util/ByteSource.java b/core/src/main/java/org/apache/shiro/util/ByteSource.java index ac9b42eba8..85c1b35bde 100644 --- a/core/src/main/java/org/apache/shiro/util/ByteSource.java +++ b/core/src/main/java/org/apache/shiro/util/ByteSource.java @@ -34,7 +34,7 @@ public interface ByteSource { * * @return the wrapped byte array. */ - public byte[] getBytes(); + byte[] getBytes(); /** * Returns the Hex-formatted String representation of the @@ -43,7 +43,7 @@ public interface ByteSource { * @return the Hex-formatted String representation of the * underlying wrapped byte array. */ - public String toHex(); + String toHex(); /** * Returns the Base 64-formatted String representation of the @@ -52,7 +52,17 @@ public interface ByteSource { * @return the Base 64-formatted String representation of the * underlying wrapped byte array. */ - public String toBase64(); + String toBase64(); + + /** + * Returns {@code true} if the underlying wrapped byte array is null or empty (zero length), {@code false} + * otherwise. + * + * @return {@code true} if the underlying wrapped byte array is null or empty (zero length), {@code false} + * otherwise. + * @since 1.2 + */ + boolean isEmpty(); /** * Utility class that can construct ByteSource instances. This is slightly nicer than needing to know the @@ -123,5 +133,59 @@ public static ByteSource bytes(File file) { public static ByteSource bytes(InputStream stream) { return new SimpleByteSource(stream); } + + /** + * Returns {@code true} if the specified object can be easily represented as a {@code ByteSource} using + * the {@link ByteSource.Util}'s default heuristics, {@code false} otherwise. + *

+ * This implementation merely returns {@link SimpleByteSource}.{@link SimpleByteSource#isCompatible(Object) isCompatible(source)}. + * + * @param source the object to test to see if it can be easily converted to ByteSource instances using default + * heuristics. + * @return {@code true} if the specified object can be easily represented as a {@code ByteSource} using + * the {@link ByteSource.Util}'s default heuristics, {@code false} otherwise. + */ + public static boolean isCompatible(Object source) { + return SimpleByteSource.isCompatible(source); + } + + /** + * Returns a {@code ByteSource} instance representing the specified byte source argument. If the argument + * cannot be easily converted to bytes (as is indicated by the {@link #isCompatible(Object)} JavaDoc), + * this method will throw an {@link IllegalArgumentException}. + * + * @param source the byte-backed instance that should be represented as a {@code ByteSource} instance. + * @return a {@code ByteSource} instance representing the specified byte source argument. + * @throws IllegalArgumentException if the argument cannot be easily converted to bytes + * (as indicated by the {@link #isCompatible(Object)} JavaDoc) + */ + public static ByteSource bytes(Object source) throws IllegalArgumentException { + if (source == null) { + return null; + } + if (!isCompatible(source)) { + String msg = "Unable to heuristically acquire bytes for object of type [" + + source.getClass().getName() + "]. If this type is indeed a byte-backed data type, you might " + + "want to write your own ByteSource implementation to extract its bytes explicitly."; + throw new IllegalArgumentException(msg); + } + if (source instanceof byte[]) { + return bytes((byte[]) source); + } else if (source instanceof ByteSource) { + return (ByteSource) source; + } else if (source instanceof char[]) { + return bytes((char[]) source); + } else if (source instanceof String) { + return bytes((String) source); + } else if (source instanceof File) { + return bytes((File) source); + } else if (source instanceof InputStream) { + return bytes((InputStream) source); + } else { + throw new IllegalStateException("Encountered unexpected byte source. This is a bug - please notify " + + "the Shiro developer list asap (the isCompatible implementation does not reflect this " + + "method's implementation)."); + } + } } } diff --git a/core/src/main/java/org/apache/shiro/util/SimpleByteSource.java b/core/src/main/java/org/apache/shiro/util/SimpleByteSource.java index 0d3e94eab3..ce27f0fe5f 100644 --- a/core/src/main/java/org/apache/shiro/util/SimpleByteSource.java +++ b/core/src/main/java/org/apache/shiro/util/SimpleByteSource.java @@ -22,7 +22,8 @@ import org.apache.shiro.codec.CodecSupport; import org.apache.shiro.codec.Hex; -import java.io.*; +import java.io.File; +import java.io.InputStream; import java.util.Arrays; /** @@ -104,10 +105,39 @@ public SimpleByteSource(InputStream stream) { this.bytes = new BytesHelper().getBytes(stream); } + /** + * Returns {@code true} if the specified object is a recognized data type that can be easily converted to + * bytes by instances of this class, {@code false} otherwise. + *

+ * This implementation returns {@code true} IFF the specified object is an instance of one of the following + * types: + *

    + *
  • {@code byte[]}
  • + *
  • {@code char[]}
  • + *
  • {@link ByteSource}
  • + *
  • {@link String}
  • + *
  • {@link File}
  • + * {@link InputStream} + *
+ * + * @param o the object to test to see if it can be easily converted to bytes by instances of this class. + * @return {@code true} if the specified object can be easily converted to bytes by instances of this class, + * {@code false} otherwise. + * @since 1.2 + */ + public static boolean isCompatible(Object o) { + return o instanceof byte[] || o instanceof char[] || o instanceof String || + o instanceof ByteSource || o instanceof File || o instanceof InputStream; + } + public byte[] getBytes() { return this.bytes; } + public boolean isEmpty() { + return this.bytes == null || this.bytes.length == 0; + } + public String toHex() { if ( this.cachedHex == null ) { this.cachedHex = Hex.encodeToString(getBytes()); @@ -127,7 +157,10 @@ public String toString() { } public int hashCode() { - return toString().hashCode(); + if (this.bytes == null || this.bytes.length == 0) { + return 0; + } + return Arrays.hashCode(this.bytes); } public boolean equals(Object o) { diff --git a/core/src/main/java/org/apache/shiro/util/StringUtils.java b/core/src/main/java/org/apache/shiro/util/StringUtils.java index 286dd0d4dc..15fd9e149d 100644 --- a/core/src/main/java/org/apache/shiro/util/StringUtils.java +++ b/core/src/main/java/org/apache/shiro/util/StringUtils.java @@ -478,4 +478,25 @@ public static Set splitToSet(String delimited, String separator) { return CollectionUtils.asSet(split); } + /** + * Returns the input argument, but ensures the first character is capitalized (if possible). + * @param in the string to uppercase the first character. + * @return the input argument, but with the first character capitalized (if possible). + * @since 1.2 + */ + public static String uppercaseFirstChar(String in) { + if (in == null || in.length() == 0) { + return in; + } + int length = in.length(); + StringBuilder sb = new StringBuilder(length); + + sb.append(Character.toUpperCase(in.charAt(0))); + if (length > 1) { + String remaining = in.substring(1); + sb.append(remaining); + } + return sb.toString(); + } + } diff --git a/core/src/test/groovy/org/apache/shiro/authc/credential/DefaultPasswordServiceTest.groovy b/core/src/test/groovy/org/apache/shiro/authc/credential/DefaultPasswordServiceTest.groovy new file mode 100644 index 0000000000..796d3471b9 --- /dev/null +++ b/core/src/test/groovy/org/apache/shiro/authc/credential/DefaultPasswordServiceTest.groovy @@ -0,0 +1,25 @@ +package org.apache.shiro.authc.credential + +import org.apache.shiro.util.ByteSource + +/** + * Created by IntelliJ IDEA. + * User: lhazlewood + * Date: 9/20/11 + * Time: 3:38 PM + * To change this template use File | Settings | File Templates. + */ +class DefaultPasswordServiceTest extends GroovyTestCase { + + void testDefault() { + def passwordService = new DefaultPasswordService(); + + def password = ByteSource.Util.bytes("12345") + + def formatted = passwordService.hashPassword(password) + System.out.println "Formatted/stored password: $formatted" + + assertTrue passwordService.passwordsMatch(password, '$shiro1$SHA-512$300000$d07mwTTz3EHqQEdc5KBPCgzigcuwYmbfD3nw7ao7zmA=$B76M6PRqOl4kaScZjKHDWVcE08MwOrqTQyqmmPAIw9Sl0ONG/Rv7GxeUfc5fA3ujhxKJgGgDllDC1EchHFlncw=='); + } + +} diff --git a/core/src/main/java/org/apache/shiro/crypto/hash/SimpleHashResponse.java b/core/src/test/groovy/org/apache/shiro/codec/H64Test.groovy similarity index 50% rename from core/src/main/java/org/apache/shiro/crypto/hash/SimpleHashResponse.java rename to core/src/test/groovy/org/apache/shiro/codec/H64Test.groovy index 86c6caf5e2..1560c47e1f 100644 --- a/core/src/main/java/org/apache/shiro/crypto/hash/SimpleHashResponse.java +++ b/core/src/test/groovy/org/apache/shiro/codec/H64Test.groovy @@ -16,37 +16,25 @@ * specific language governing permissions and limitations * under the License. */ -package org.apache.shiro.crypto.hash; +package org.apache.shiro.codec -import org.apache.shiro.util.ByteSource; +import org.apache.shiro.crypto.SecureRandomNumberGenerator /** - * Simple implementation of {@link HashResponse} that retains the {@link #getHash hash} and - * {@link #getSalt hashSalt} properties as private attributes. - * - * @since 1.2 + * Test cases for the {@link H64} implementation. */ -public class SimpleHashResponse implements HashResponse { +class H64Test extends GroovyTestCase { - private final Hash hash; - private final ByteSource salt; + void testNothing(){} - /** - * Constructs a new instance with the specified hash and salt. - * - * @param hash the hash to respond with. - * @param salt the public salt associated with the specified hash. - */ - public SimpleHashResponse(Hash hash, ByteSource salt) { - this.hash = hash; - this.salt = salt; - } + public void testDefault() { + byte[] orig = new SecureRandomNumberGenerator().nextBytes(6).bytes - public Hash getHash() { - return this.hash; - } + System.out.println("bytes: $orig"); + + String encoded = H64.encodeToString(orig) + System.out.println("encoded: $encoded"); - public ByteSource getSalt() { - return this.salt; + assertNotNull orig } } diff --git a/core/src/test/groovy/org/apache/shiro/crypto/hash/DefaultHasherTest.groovy b/core/src/test/groovy/org/apache/shiro/crypto/hash/DefaultHasherTest.groovy deleted file mode 100644 index 89705acc5d..0000000000 --- a/core/src/test/groovy/org/apache/shiro/crypto/hash/DefaultHasherTest.groovy +++ /dev/null @@ -1,50 +0,0 @@ -/* - * 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.crypto.hash - -/** - * Created by IntelliJ IDEA. - * User: lhazlewood - * Date: 4/3/11 - * Time: 3:21 AM - * To change this template use File | Settings | File Templates. - */ -class DefaultHasherTest extends GroovyTestCase { - - void testDefault() { - /*DefaultHasher hasher = new DefaultHasher() - SecureRandomNumberGenerator rng = new SecureRandomNumberGenerator(); - - long n = 200000 - def times = [n] - for (int i = 0; i < n; i++) { - long start = System.currentTimeMillis() - hasher.computeHash new SimpleByteSource(rng.nextBytes(16)) - long stop = System.currentTimeMillis() - times << stop - start - - } - - long total = 0; - for( long l : times) { - total += l - } - System.out.println(total / n); */ - } -} diff --git a/core/src/test/groovy/org/apache/shiro/crypto/hash/format/Shiro1CryptFormatTest.groovy b/core/src/test/groovy/org/apache/shiro/crypto/hash/format/Shiro1CryptFormatTest.groovy new file mode 100644 index 0000000000..2228464e13 --- /dev/null +++ b/core/src/test/groovy/org/apache/shiro/crypto/hash/format/Shiro1CryptFormatTest.groovy @@ -0,0 +1,158 @@ +/* + * 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.crypto.hash.format + +import org.apache.shiro.crypto.SecureRandomNumberGenerator +import org.apache.shiro.crypto.hash.SimpleHash + +/** + * Unit tests for the {@link Shiro1CryptFormat} implementation. + * + * @since 1.2 + */ +class Shiro1CryptFormatTest extends GroovyTestCase { + + void testGetId() { + assertEquals "shiro1", new Shiro1CryptFormat().getId() + } + + void testFormatDefault() { + def format = new Shiro1CryptFormat(); + + def alg = "SHA-512" + def iterations = 10; + def rng = new SecureRandomNumberGenerator() + def source = rng.nextBytes() + def salt = rng.nextBytes() + + def hash = new SimpleHash(alg, source, salt, iterations) + + String formatted = format.format(hash); + + String expected = + Shiro1CryptFormat.MCF_PREFIX + alg + '$' + iterations + '$' + salt.toBase64() + '$' + hash.toBase64() + + assertNotNull formatted + assertEquals expected, formatted + } + + void testFormatWithoutSalt() { + def format = new Shiro1CryptFormat(); + + def alg = "SHA-512" + def iterations = 10; + def rng = new SecureRandomNumberGenerator() + def source = rng.nextBytes() + + def hash = new SimpleHash(alg, source, null, iterations) + + String formatted = format.format(hash); + + String expected = Shiro1CryptFormat.MCF_PREFIX + alg + '$' + iterations + '$$' + hash.toBase64() + + assertNotNull formatted + assertEquals expected, formatted + } + + void testFormatWithNullArgument() { + def format = new Shiro1CryptFormat() + def result = format.format(null) + assertNull result + } + + void testParseDefault() { + def format = new Shiro1CryptFormat(); + def delim = Shiro1CryptFormat.TOKEN_DELIMITER + + def alg = "SHA-512" + def iterations = 10; + def rng = new SecureRandomNumberGenerator() + def source = rng.nextBytes() + def salt = rng.nextBytes() + + def hash = new SimpleHash(alg, source, salt, iterations) + + String formatted = Shiro1CryptFormat.MCF_PREFIX + + alg + delim + + iterations + delim + + salt.toBase64() + delim + + hash.toBase64() + + def parsedHash = format.parse(formatted) + + assertEquals hash, parsedHash + assertEquals hash.algorithmName, parsedHash.algorithmName + assertEquals hash.iterations, parsedHash.iterations + assertEquals hash.salt, parsedHash.salt + assertTrue Arrays.equals(hash.bytes, parsedHash.bytes) + } + + void testParseWithoutSalt() { + def format = new Shiro1CryptFormat(); + def delim = Shiro1CryptFormat.TOKEN_DELIMITER + + def alg = "SHA-512" + def iterations = 10; + def rng = new SecureRandomNumberGenerator() + def source = rng.nextBytes() + + def hash = new SimpleHash(alg, source, null, iterations) + + String formatted = Shiro1CryptFormat.MCF_PREFIX + + alg + delim + + iterations + delim + + delim + + hash.toBase64() + + def parsedHash = format.parse(formatted) + + assertEquals hash, parsedHash + assertEquals hash.algorithmName, parsedHash.algorithmName + assertEquals hash.iterations, parsedHash.iterations + assertNull hash.salt + assertTrue Arrays.equals(hash.bytes, parsedHash.bytes) + } + + void testParseWithNullArgument() { + def format = new Shiro1CryptFormat() + def result = format.parse(null) + assertNull result + } + + void testParseWithInvalidId() { + def format = new Shiro1CryptFormat() + try { + format.parse('$foo$xxxxxxx') + fail("parse should have thrown an IllegalArgumentException") + } catch (IllegalArgumentException expected) { + } + } + + void testParseWithNonNumericIterations() { + def format = new Shiro1CryptFormat(); + def formatted = '$shiro1$SHA-512$N$foo$foo' + + try { + format.parse(formatted) + fail("parse should have thrown an IllegalArgumentException") + } catch (IllegalArgumentException expected) { + } + } + +} diff --git a/core/src/test/java/org/apache/shiro/crypto/hash/DefaultHashServiceTest.java b/core/src/test/java/org/apache/shiro/crypto/hash/DefaultHashServiceTest.java new file mode 100644 index 0000000000..647e378895 --- /dev/null +++ b/core/src/test/java/org/apache/shiro/crypto/hash/DefaultHashServiceTest.java @@ -0,0 +1,152 @@ +package org.apache.shiro.crypto.hash; + +import junit.framework.TestCase; +import org.apache.shiro.crypto.SecureRandomNumberGenerator; +import org.junit.Test; + +import java.util.Arrays; + +/** + * Test for {@link DefaultHashService} class. + * + */ +public class DefaultHashServiceTest { + + /** + * If the same string is hashed twice and no salt was supplied, hashed + * result should be different in each case. + */ + @Test + public void testOnlyRandomSaltRandomness() { + HashService hashService = createHashService(); + + Hash firstHash = hashString(hashService, "password"); + Hash secondHash = hashString(hashService, "password"); + + assertNotEquals(firstHash.toBase64(), secondHash.toBase64()); + } + + /** + * If a string is hashed and no salt was supplied, random salt is generated. + * Hash of the same string with generated random salt should return the + * same result. + */ + @Test + public void testOnlyRandomSaltReturn() { + HashService hashService = createHashService(); + + Hash firstHash = hashString(hashService, "password"); + Hash secondHash = hashString(hashService, "password", firstHash.getSalt().getBytes()); + + TestCase.assertEquals(firstHash.toBase64(), secondHash.toBase64()); + } + + /** + * Two different strings hashed with the same salt should result in two different + * hashes. + */ + @Test + public void testOnlyRandomSaltHash() { + HashService hashService = createHashService(); + + Hash firstHash = hashString(hashService, "password"); + Hash secondHash = hashString(hashService, "password2", firstHash.getSalt().getBytes()); + + assertNotEquals(firstHash.toBase64(), secondHash.toBase64()); + } + + /** + * If the same string is hashed twice and only base salt was supplied, hashed + * result should be different in each case. + */ + @Test + public void testBothSaltsRandomness() { + HashService hashService = createHashServiceWithSalt(); + + Hash firstHash = hashString(hashService, "password"); + Hash secondHash = hashString(hashService, "password"); + + assertNotEquals(firstHash.toBase64(), secondHash.toBase64()); + } + + /** + * If a string is hashed and only base salt was supplied, random salt is generated. + * Hash of the same string with generated random salt should return the + * same result. + */ + @Test + public void testBothSaltsReturn() { + HashService hashService = createHashServiceWithSalt(); + + Hash firstHash = hashString(hashService, "password"); + Hash secondHash = hashString(hashService, "password", firstHash.getSalt().getBytes()); + + TestCase.assertEquals(firstHash.toBase64(), secondHash.toBase64()); + } + + /** + * Two different strings hashed with the same salt should result in two different + * hashes. + */ + @Test + public void testBothSaltsHash() { + HashService hashService = createHashServiceWithSalt(); + + Hash firstHash = hashString(hashService, "password"); + Hash secondHash = hashString(hashService, "password2", firstHash.getSalt().getBytes()); + + assertNotEquals(firstHash.toBase64(), secondHash.toBase64()); + } + + /** + * Hash result is different if the base salt is added. + */ + @Test + public void testBaseSaltChangesResult() { + HashService saltedhashService = createHashServiceWithSalt(); + HashService hashService = createHashService(); + + Hash firstHash = hashStringPredictable(saltedhashService, "password"); + Hash secondHash = hashStringPredictable(hashService, "password"); + + assertNotEquals(firstHash.toBase64(), secondHash.toBase64()); + } + + protected Hash hashString(HashService hashService, String string) { + return hashService.computeHash(new HashRequest.Builder().setSource(string).build()); + } + + protected Hash hashString(HashService hashService, String string, byte[] salt) { + return hashService.computeHash(new HashRequest.Builder().setSource(string).setSalt(salt).build()); + } + + private Hash hashStringPredictable(HashService hashService, String string) { + byte[] salt = new byte[20]; + Arrays.fill(salt, (byte) 2); + return hashService.computeHash(new HashRequest.Builder().setSource(string).setSalt(salt).build()); + } + + private HashService createHashService() { + return new DefaultHashService(); + } + + private HashService createHashServiceWithSalt() { + DefaultHashService defaultHashService = new DefaultHashService(); + defaultHashService.setPrivateSalt(new SecureRandomNumberGenerator().nextBytes()); + + return defaultHashService; + } + + private void assertNotEquals(String str1, String str2) { + boolean equals = equals(str1, str2); + if (equals) + TestCase.fail("Strings are supposed to be different."); + } + + protected boolean equals(String str1, String str2) { + if (str1 == null) + return str2 == null; + + return str1.equals(str2); + } +} diff --git a/core/src/test/java/org/apache/shiro/crypto/hash/DefaultHasherTest.java b/core/src/test/java/org/apache/shiro/crypto/hash/DefaultHasherTest.java deleted file mode 100644 index ba14454538..0000000000 --- a/core/src/test/java/org/apache/shiro/crypto/hash/DefaultHasherTest.java +++ /dev/null @@ -1,154 +0,0 @@ -package org.apache.shiro.crypto.hash; - -import java.util.Arrays; - -import junit.framework.TestCase; - -import org.apache.shiro.crypto.SecureRandomNumberGenerator; -import org.apache.shiro.util.ByteSource; -import org.junit.Test; - -/** - * Test for {@link DefaultHasher} class. - * - */ -public class DefaultHasherTest { - - /** - * If the same string is hashed twice and no salt was supplied, hashed - * result should be different in each case. - */ - @Test - public void testOnlyRandomSaltRandomness() { - Hasher hasher = createHasher(); - - HashResponse firstHash = hashString(hasher, "password"); - HashResponse secondHash = hashString(hasher, "password"); - - assertNotEquals(firstHash.getHash().toBase64(), secondHash.getHash().toBase64()); - } - - /** - * If a string is hashed and no salt was supplied, random salt is generated. - * Hash of the same string with generated random salt should return the - * same result. - */ - @Test - public void testOnlyRandomSaltReturn() { - Hasher hasher = createHasher(); - - HashResponse firstHash = hashString(hasher, "password"); - HashResponse secondHash = hashString(hasher, "password", firstHash.getSalt().getBytes()); - - TestCase.assertEquals(firstHash.getHash().toBase64(), secondHash.getHash().toBase64()); - } - - /** - * Two different strings hashed with the same salt should result in two different - * hashes. - */ - @Test - public void testOnlyRandomSaltHash() { - Hasher hasher = createHasher(); - - HashResponse firstHash = hashString(hasher, "password"); - HashResponse secondHash = hashString(hasher, "password2", firstHash.getSalt().getBytes()); - - assertNotEquals(firstHash.getHash().toBase64(), secondHash.getHash().toBase64()); - } - - /** - * If the same string is hashed twice and only base salt was supplied, hashed - * result should be different in each case. - */ - @Test - public void testBothSaltsRandomness() { - Hasher hasher = createHasherWithSalt(); - - HashResponse firstHash = hashString(hasher, "password"); - HashResponse secondHash = hashString(hasher, "password"); - - assertNotEquals(firstHash.getHash().toBase64(), secondHash.getHash().toBase64()); - } - - /** - * If a string is hashed and only base salt was supplied, random salt is generated. - * Hash of the same string with generated random salt should return the - * same result. - */ - @Test - public void testBothSaltsReturn() { - Hasher hasher = createHasherWithSalt(); - - HashResponse firstHash = hashString(hasher, "password"); - HashResponse secondHash = hashString(hasher, "password", firstHash.getSalt().getBytes()); - - TestCase.assertEquals(firstHash.getHash().toBase64(), secondHash.getHash().toBase64()); - } - - /** - * Two different strings hashed with the same salt should result in two different - * hashes. - */ - @Test - public void testBothSaltsHash() { - Hasher hasher = createHasherWithSalt(); - - HashResponse firstHash = hashString(hasher, "password"); - HashResponse secondHash = hashString(hasher, "password2", firstHash.getSalt().getBytes()); - - assertNotEquals(firstHash.getHash().toBase64(), secondHash.getHash().toBase64()); - } - - /** - * Hash result is different if the base salt is added. - */ - @Test - public void testBaseSaltChangesResult() { - Hasher saltedHasher = createHasherWithSalt(); - Hasher hasher = createHasher(); - - HashResponse firstHash = hashStringPredictable(saltedHasher, "password"); - HashResponse secondHash = hashStringPredictable(hasher, "password"); - - assertNotEquals(firstHash.getHash().toBase64(), secondHash.getHash().toBase64()); - } - - protected HashResponse hashString(Hasher hasher, String string) { - return hasher.computeHash(new SimpleHashRequest(ByteSource.Util.bytes(string))); - } - - protected HashResponse hashString(Hasher hasher, String string, byte[] salt) { - return hasher.computeHash(new SimpleHashRequest(ByteSource.Util.bytes(string), ByteSource.Util.bytes(salt))); - } - - private HashResponse hashStringPredictable(Hasher hasher, String string) { - byte[] salt = new byte[20]; - Arrays.fill(salt, (byte) 2); - return hasher.computeHash(new SimpleHashRequest(ByteSource.Util.bytes(string), ByteSource.Util.bytes(salt))); - } - - private Hasher createHasher() { - return new DefaultHasher(); - } - - private Hasher createHasherWithSalt() { - DefaultHasher defaultHasher = new DefaultHasher(); - defaultHasher.setBaseSalt((new SecureRandomNumberGenerator()).nextBytes().getBytes()); - - return defaultHasher; - } - - private void assertNotEquals(String str1, String str2) { - boolean equals = equals(str1, str2); - if (equals) - TestCase.fail("Strings are supposed to be different."); - } - - protected boolean equals(String str1, String str2) { - if (str1 == null) - return str2 == null; - - return str1.equals(str2); - } -} diff --git a/tools/hasher/src/main/java/org/apache/shiro/tools/hasher/Hasher.java b/tools/hasher/src/main/java/org/apache/shiro/tools/hasher/Hasher.java index 091e50d329..50c1963c08 100644 --- a/tools/hasher/src/main/java/org/apache/shiro/tools/hasher/Hasher.java +++ b/tools/hasher/src/main/java/org/apache/shiro/tools/hasher/Hasher.java @@ -24,6 +24,7 @@ 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; @@ -46,28 +47,30 @@ */ public final class Hasher { - private static final Option ALGORITHM = new Option("a", "algorithm", true, "hash algorithm name. Defaults to MD5."); + private static final Option ALGORITHM = new Option("a", "algorithm", true, "hash algorithm name. Defaults to MD5 (SHA-256 when password hashing)."); 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 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 ITERATIONS = new Option("i", "iterations", true, "number of hash iterations. Defaults to 350,000 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 . See below for more information."); private static final Option SALT = new Option("s", "salt", true, "use the specified salt. is plaintext."); private static final Option SALT_BYTES = new Option("sb", "saltbytes", true, "use the specified salt bytes. 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 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 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 String DEFAULT_PASSWORD_ALGORITHM_NAME = "SHA-256"; 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 = 350000; 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"); @@ -81,26 +84,23 @@ public static void main(String[] args) { CommandLineParser parser = new PosixParser(); Options options = new Options(); - options.addOption(HELP).addOption(DEBUG).addOption(ALGORITHM).addOption(HEX).addOption(ITERATIONS); + 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(SALT_GEN_HEX); - options.addOption(NO_FORMAT).addOption(SHIRO); + 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 = DEFAULT_ALGORITHM_NAME; - int iterations = DEFAULT_NUM_ITERATIONS; - boolean base64 = true; + 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; - boolean generatedSaltBase64 = true; int generatedSaltSize = DEFAULT_GENERATED_SALT_SIZE; - boolean shiroFormat = false; - boolean format = true; + String formatString = null; char[] passwordChars = null; @@ -119,17 +119,16 @@ public static void main(String[] args) { if (line.hasOption(ITERATIONS.getOpt())) { iterations = getRequiredPositiveInt(line, ITERATIONS); } - if (line.hasOption(HEX.getOpt())) { - base64 = false; - } 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())) { @@ -138,12 +137,11 @@ public static void main(String[] args) { if (line.hasOption(SALT_BYTES.getOpt())) { saltBytesString = line.getOptionValue(SALT_BYTES.getOpt()); } - if (line.hasOption(SALT_GEN.getOpt())) { - generateSalt = true; + if (line.hasOption(NO_SALT_GEN.getOpt())) { + generateSalt = false; } - if (line.hasOption(SALT_GEN_HEX.getOpt())) { + if (line.hasOption(SALT_GEN.getOpt())) { generateSalt = true; - generatedSaltBase64 = false; } if (line.hasOption(SALT_GEN_SIZE.getOpt())) { generateSalt = true; @@ -152,14 +150,11 @@ public static void main(String[] args) { 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; + if (line.hasOption(FORMAT.getOpt())) { + formatString = line.getOptionValue(FORMAT.getOpt()); } - String sourceValue = null; + String sourceValue; Object source; @@ -186,19 +181,45 @@ public static void main(String[] args) { } } + 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); - StringBuilder output; - if (shiroFormat) { - output = formatForShiroIni(hash, base64, salt, generatedSaltBase64, generateSalt); - } else if (format) { - output = format(hash, base64, salt, generatedSaltBase64, generateSalt, algorithm, sourceValue); - } else { - output = formatMinimal(hash, base64, salt, generatedSaltBase64, generateSalt); + 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) { @@ -238,72 +259,6 @@ private static void exit(Exception e, boolean 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, Option option) { String iterVal = line.getOptionValue(option.getOpt()); try { @@ -387,7 +342,6 @@ private static void printHelp(Options options, Exception e, boolean debug) { "---------------------------------\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" + @@ -401,9 +355,9 @@ private static void printHelp(Options options, Exception e, boolean debug) { "\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" + + "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." + + "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" + @@ -424,7 +378,18 @@ private static void printHelp(Options options, Exception e, boolean debug) { " -r ~/documents/myfile.pdf\n" + " -r /usr/local/logs/absolutePathFile.log\n" + " -r url:http://foo.com/page.html\n" + - " -r classpath:/WEB-INF/lib/something.jar"; + " -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); diff --git a/web/src/main/java/org/apache/shiro/web/env/DefaultWebEnvironment.java b/web/src/main/java/org/apache/shiro/web/env/DefaultWebEnvironment.java index 4e1cab30f8..d5658ab493 100644 --- a/web/src/main/java/org/apache/shiro/web/env/DefaultWebEnvironment.java +++ b/web/src/main/java/org/apache/shiro/web/env/DefaultWebEnvironment.java @@ -42,11 +42,11 @@ public DefaultWebEnvironment() { } public FilterChainResolver getFilterChainResolver() { - return (FilterChainResolver)this.objects.get(DEFAULT_FILTER_CHAIN_RESOLVER_NAME); + return getObject(DEFAULT_FILTER_CHAIN_RESOLVER_NAME, FilterChainResolver.class); } public void setFilterChainResolver(FilterChainResolver filterChainResolver) { - this.objects.put(DEFAULT_FILTER_CHAIN_RESOLVER_NAME, filterChainResolver); + setObject(DEFAULT_FILTER_CHAIN_RESOLVER_NAME, filterChainResolver); } @Override diff --git a/web/src/test/java/org/apache/shiro/web/mgt/CookieRememberMeManagerTest.java b/web/src/test/java/org/apache/shiro/web/mgt/CookieRememberMeManagerTest.java index 65eb848da4..1144575673 100644 --- a/web/src/test/java/org/apache/shiro/web/mgt/CookieRememberMeManagerTest.java +++ b/web/src/test/java/org/apache/shiro/web/mgt/CookieRememberMeManagerTest.java @@ -35,13 +35,10 @@ import javax.servlet.http.HttpServletResponse; import static org.easymock.EasyMock.*; -import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertNull; -import static org.junit.Assert.assertTrue; -import static org.junit.Assert.fail; +import static org.junit.Assert.*; /** - * TODO - Class JavaDoc + * Unit tests for the {@link CookieRememberMeManager} implementation. * * @since 1.0 */ @@ -125,13 +122,13 @@ public void getRememberedPrincipals() { //The following base64 string was determined from the log output of the above 'onSuccessfulLogin' test. //This will have to change any time the PrincipalCollection implementation changes: - final String userPCAesBase64 = "qk7spFqO1zoNLgq3qArE7bc8+J+Zvm1jz8lDSUmRiRlDQQx7jxG4+" + - "QImiRpR7zO0d9oHH+7C3VeN9OvGMdjxtpbInMsLcGz4Q0u3M1fmyErn5Mr61chmNzQ8cLegpIKE3M+xMY" + - "5JB1PRw7aEJdRxtHh80kiXZ5jeALvDP3hmFM7OF2CDKLIIa83XuBQvyrKGI9GhsxGTLkmNFknbfRsmN7v" + - "NIDorceeaMkAetYf6GxDOw1ZK7yEbsydIHnqVWNHLen6DHC8pLkqMNOoGwXLeBroD6mRpoFf76J0VKBcd" + - "C54Mg73S2R7wx9ZzSNJJrCi1KAilmThzm3Rm97EidUnYlWI0TM+zvMzNsLynIK4PoIG6HYQQfEI35qVRI" + - "bCdbTlTnjfM/fPf7RWO8s4Z7KzszSQMJE9LgBudcyzrld5ZrWb11cianskNZMI8kzOITezjjqvWn5U4jg" + - "Mb9a6qcpaNJcgaxV6NZRmof8cnet54wwE="; + final String userPCAesBase64 = "WlD5MLzzZznN3dQ1lPJO/eScSuY245k29aECNmjUs31o7Yu478hWhaM5Sj" + + "jmoe900/72JNu3hcJaPG6Q17Vuz4F8x0kBjbFnPVx4PqzsZYT6yreeS2jwO6OwfI+efqXOKyB2a5KPtnr" + + "7jt5kZsyH38XJISb81cf6xqTGUru8zC+kNqJFz7E5RpO0kraBofS5jhMm45gDVjDRkjgPJAzocVWMtrza" + + "zy67P8eb+kMSBCqGI251JTNAGboVgQ28KjfaAJ/6LXRJUj7kB7CGia7mgRk+hxzEJGDs81at5VOPqODJr" + + "xb8tcIdemFUFIkiYVP9bGs4dP3ECtmw7aNrCzv+84sx3vRFUrd5DbDYpEuE12hF2Y9owDK9sxStbXoF0y" + + "A32dhfGDIqS+agsass0sWn8WX2TM9i8SxrUjiFbxqyIG49HbqGrZp5QLM9IuIwO+TzGfF1FzumQGdwmWT" + + "xkVapw5UESl34YvA615cb+82ue1I="; Cookie[] cookies = new Cookie[]{ new Cookie(CookieRememberMeManager.DEFAULT_REMEMBER_ME_COOKIE_NAME, userPCAesBase64)