Skip to content

Commit

Permalink
SHIRO-213, SHIRO-279, SHIRO-280, SHIRO-302: Added PasswordService and…
Browse files Browse the repository at this point in the history
… supporting implementations, HashService (renamed from Hasher to avoid confusion/conflict w/ the command-line Hasher) and supporting implementations, a new HashFormat interface and supporting implementations. More tests and documentation to follow today and later this week.

git-svn-id: https://svn.apache.org/repos/asf/shiro/trunk@1209662 13f79535-47bb-0310-9956-ffa450edef68
  • Loading branch information
Les Hazlewood committed Dec 2, 2011
1 parent 5ea1669 commit cd0efd9
Show file tree
Hide file tree
Showing 41 changed files with 2,542 additions and 943 deletions.
39 changes: 38 additions & 1 deletion RELEASE-NOTES
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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
###########################################################
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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):
* <ul>
* <li>{@code hex}</li>
* <li>{@code base64}</li>
* <li>{@code base-64}</li>
* </ul>
* {@code false} otherwise.
* <p/>
* 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;
}
}
Original file line number Diff line number Diff line change
@@ -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.
* <p/>
* 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;
}
}
Loading

0 comments on commit cd0efd9

Please sign in to comment.