Author: shane.bryzak(a)jboss.com
Date: 2009-04-14 19:56:22 -0400 (Tue, 14 Apr 2009)
New Revision: 10417
Added:
trunk/src/main/org/jboss/seam/annotations/security/management/PasswordSalt.java
Modified:
trunk/src/main/org/jboss/seam/annotations/security/management/UserPassword.java
trunk/src/main/org/jboss/seam/security/management/JpaIdentityStore.java
trunk/src/main/org/jboss/seam/security/management/PasswordHash.java
Log:
JBSEAM-3762
Added: trunk/src/main/org/jboss/seam/annotations/security/management/PasswordSalt.java
===================================================================
--- trunk/src/main/org/jboss/seam/annotations/security/management/PasswordSalt.java
(rev 0)
+++
trunk/src/main/org/jboss/seam/annotations/security/management/PasswordSalt.java 2009-04-14
23:56:22 UTC (rev 10417)
@@ -0,0 +1,24 @@
+package org.jboss.seam.annotations.security.management;
+
+import static java.lang.annotation.ElementType.FIELD;
+import static java.lang.annotation.ElementType.METHOD;
+import static java.lang.annotation.RetentionPolicy.RUNTIME;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.Inherited;
+import java.lang.annotation.Retention;
+import java.lang.annotation.Target;
+
+/**
+ * The salt value used to produce the password hash
+ *
+ * @author Shane Bryzak
+ */
+@Target({METHOD,FIELD})
+@Documented
+@Retention(RUNTIME)
+@Inherited
+public @interface PasswordSalt
+{
+
+}
Modified: trunk/src/main/org/jboss/seam/annotations/security/management/UserPassword.java
===================================================================
---
trunk/src/main/org/jboss/seam/annotations/security/management/UserPassword.java 2009-04-14
21:36:54 UTC (rev 10416)
+++
trunk/src/main/org/jboss/seam/annotations/security/management/UserPassword.java 2009-04-14
23:56:22 UTC (rev 10417)
@@ -20,5 +20,13 @@
@Inherited
public @interface UserPassword
{
+ /**
+ * The hash algorithm, only used if there is no @PasswordSalt property specified
+ */
String hash() default "";
+
+ /**
+ * Number of iterations for generating the password hash
+ */
+ int iterations() default 1000;
}
Modified: trunk/src/main/org/jboss/seam/security/management/JpaIdentityStore.java
===================================================================
--- trunk/src/main/org/jboss/seam/security/management/JpaIdentityStore.java 2009-04-14
21:36:54 UTC (rev 10416)
+++ trunk/src/main/org/jboss/seam/security/management/JpaIdentityStore.java 2009-04-14
23:56:22 UTC (rev 10417)
@@ -6,7 +6,9 @@
import java.io.Serializable;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
+import java.security.GeneralSecurityException;
import java.security.Principal;
+import java.security.SecureRandom;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
@@ -22,6 +24,7 @@
import org.jboss.seam.annotations.Observer;
import org.jboss.seam.annotations.Scope;
import org.jboss.seam.annotations.intercept.BypassInterceptors;
+import org.jboss.seam.annotations.security.management.PasswordSalt;
import org.jboss.seam.annotations.security.management.RoleConditional;
import org.jboss.seam.annotations.security.management.RoleGroups;
import org.jboss.seam.annotations.security.management.RoleName;
@@ -41,6 +44,7 @@
import org.jboss.seam.security.Role;
import org.jboss.seam.security.SimplePrincipal;
import org.jboss.seam.util.AnnotatedBeanProperty;
+import org.jboss.seam.util.Base64;
import org.jboss.seam.util.TypedBeanProperty;
/**
@@ -76,6 +80,7 @@
private AnnotatedBeanProperty<UserPrincipal> userPrincipalProperty;
private AnnotatedBeanProperty<UserPassword> userPasswordProperty;
+ private AnnotatedBeanProperty<PasswordSalt> passwordSaltProperty;
private AnnotatedBeanProperty<UserRoles> userRolesProperty;
private AnnotatedBeanProperty<UserEnabled> userEnabledProperty;
private AnnotatedBeanProperty<UserFirstName> userFirstNameProperty;
@@ -126,6 +131,7 @@
{
userPrincipalProperty = new AnnotatedBeanProperty(userClass, UserPrincipal.class);
userPasswordProperty = new AnnotatedBeanProperty(userClass, UserPassword.class);
+ passwordSaltProperty = new AnnotatedBeanProperty(userClass, PasswordSalt.class);
userRolesProperty = new AnnotatedBeanProperty(userClass, UserRoles.class);
userEnabledProperty = new AnnotatedBeanProperty(userClass, UserEnabled.class);
userFirstNameProperty = new AnnotatedBeanProperty(userClass, UserFirstName.class);
@@ -217,8 +223,8 @@
if (userEnabledProperty.isSet()) userEnabledProperty.setValue(user, false);
}
else
- {
- userPasswordProperty.setValue(user, generatePasswordHash(password,
getUserAccountSalt(user)));
+ {
+ setUserPassword(user, password);
if (userEnabledProperty.isSet()) userEnabledProperty.setValue(user, true);
}
@@ -243,12 +249,38 @@
}
}
+ protected void setUserPassword(Object user, String password)
+ {
+ if (passwordSaltProperty.isSet())
+ {
+ byte[] salt = generateUserSalt(user);
+ passwordSaltProperty.setValue(user, Base64.encodeBytes(salt));
+ userPasswordProperty.setValue(user, generatePasswordHash(password, salt));
+ }
+ else
+ {
+ userPasswordProperty.setValue(user, generatePasswordHash(password,
getUserAccountSalt(user)));
+ }
+ }
+
+ /**
+ * @deprecated Use JpaIdentityStore.generateRandomSalt(Object) instead
+ */
+ @Deprecated
protected String getUserAccountSalt(Object user)
- {
+ {
// By default, we'll use the user's username as the password salt
return userPrincipalProperty.getValue(user).toString();
}
+ /**
+ * Generates a 64 bit random salt value
+ */
+ public byte[] generateUserSalt(Object user)
+ {
+ return PasswordHash.instance().generateRandomSalt();
+ }
+
public boolean createUser(String username, String password)
{
return createUser(username, password, null, null);
@@ -361,8 +393,6 @@
}
}
- mergeEntity(user);
-
return true;
}
@@ -399,8 +429,7 @@
}
}
}
-
- if (success) mergeEntity(user);
+
return success;
}
@@ -420,7 +449,6 @@
throw new NoSuchRoleException("Could not grant role, group '" +
group + "' does not exist");
}
-
Collection roleGroups = (Collection) roleGroupsProperty.getValue(targetRole);
if (roleGroups == null)
{
@@ -452,7 +480,6 @@
}
((Collection) roleGroupsProperty.getValue(targetRole)).add(targetGroup);
- mergeEntity(targetRole);
return true;
}
@@ -474,8 +501,6 @@
}
boolean success = ((Collection)
roleGroupsProperty.getValue(roleToRemove)).remove(targetGroup);
-
- if (success) mergeEntity(roleToRemove);
return success;
}
@@ -565,8 +590,7 @@
return false;
}
- userEnabledProperty.setValue(user, true);
- mergeEntity(user);
+ userEnabledProperty.setValue(user, true);
return true;
}
@@ -590,9 +614,7 @@
return false;
}
- userEnabledProperty.setValue(user, false);
- mergeEntity(user);
-
+ userEnabledProperty.setValue(user, false);
return true;
}
@@ -604,8 +626,8 @@
throw new NoSuchUserException("Could not change password, user '"
+ username + "' does not exist");
}
- userPasswordProperty.setValue(user, generatePasswordHash(password,
getUserAccountSalt(user)));
- mergeEntity(user);
+ setUserPassword(user, password);
+
return true;
}
@@ -723,8 +745,33 @@
}
}
+ public String generatePasswordHash(String password, byte[] salt)
+ {
+ if (passwordSaltProperty.isSet())
+ {
+ try
+ {
+ return PasswordHash.instance().createPasswordKey(password.toCharArray(),
salt,
+ userPasswordProperty.getAnnotation().iterations());
+ }
+ catch (GeneralSecurityException ex)
+ {
+ throw new IdentityManagementException("Exception generating password
hash", ex);
+ }
+ }
+ else
+ {
+ return generatePasswordHash(password, new String(salt));
+ }
+ }
+
+ /**
+ *
+ * @deprecated Use JpaIdentityStore.generatePasswordHash(String, byte[]) instead
+ */
+ @Deprecated
protected String generatePasswordHash(String password, String salt)
- {
+ {
String algorithm = userPasswordProperty.getAnnotation().hash();
if (algorithm == null || "".equals(algorithm))
@@ -752,7 +799,7 @@
{
return PasswordHash.instance().generateSaltedHash(password, salt,
algorithm);
}
- }
+ }
}
public boolean authenticate(String username, String password)
@@ -763,7 +810,25 @@
return false;
}
- String passwordHash = generatePasswordHash(password, getUserAccountSalt(user));
+ String passwordHash = null;
+
+ if (passwordSaltProperty.isSet())
+ {
+ String encodedSalt = (String) passwordSaltProperty.getValue(user);
+ if (encodedSalt == null)
+ {
+ throw new IdentityManagementException("A @PasswordSalt property was
found on entity " + user +
+ ", but it contains no value");
+ }
+
+ passwordHash = generatePasswordHash(password, Base64.decode(encodedSalt));
+ }
+ else
+ {
+ passwordHash = generatePasswordHash(password, getUserAccountSalt(user));
+ }
+
+
boolean success = passwordHash.equals(userPasswordProperty.getValue(user));
if (success && Events.exists())
Modified: trunk/src/main/org/jboss/seam/security/management/PasswordHash.java
===================================================================
--- trunk/src/main/org/jboss/seam/security/management/PasswordHash.java 2009-04-14
21:36:54 UTC (rev 10416)
+++ trunk/src/main/org/jboss/seam/security/management/PasswordHash.java 2009-04-14
23:56:22 UTC (rev 10417)
@@ -3,8 +3,15 @@
import static org.jboss.seam.ScopeType.STATELESS;
import static org.jboss.seam.annotations.Install.BUILT_IN;
+import java.security.GeneralSecurityException;
import java.security.MessageDigest;
+import java.security.SecureRandom;
+import javax.crypto.SecretKey;
+import javax.crypto.SecretKeyFactory;
+import javax.crypto.spec.PBEKeySpec;
+import javax.crypto.spec.SecretKeySpec;
+
import org.jboss.seam.Component;
import org.jboss.seam.ScopeType;
import org.jboss.seam.annotations.Install;
@@ -26,24 +33,33 @@
{
public static final String ALGORITHM_MD5 = "MD5";
public static final String ALGORITHM_SHA = "SHA";
-
+
private static final String DEFAULT_ALGORITHM = ALGORITHM_MD5;
+ private int saltLength = 8; // default password salt length, in bytes
+
+ @Deprecated
public String generateHash(String password)
{
return generateHash(password, DEFAULT_ALGORITHM);
}
+ @Deprecated
public String generateHash(String password, String algorithm)
{
return generateSaltedHash(password, null, algorithm);
}
+ @Deprecated
public String generateSaltedHash(String password, String saltPhrase)
{
return generateSaltedHash(password, saltPhrase, DEFAULT_ALGORITHM);
}
+ /**
+ * @deprecated Use PasswordHash.createPasswordKey() instead
+ */
+ @Deprecated
public String generateSaltedHash(String password, String saltPhrase, String
algorithm)
{
try {
@@ -71,8 +87,39 @@
}
}
+ public byte[] generateRandomSalt()
+ {
+ byte[] salt = new byte[saltLength];
+ new SecureRandom().nextBytes(salt);
+ return salt;
+ }
+
+ /**
+ *
+ */
+ public String createPasswordKey(char[] password, byte[] salt, int iterations)
+ throws GeneralSecurityException
+ {
+ PBEKeySpec passwordKeySpec = new PBEKeySpec(password, salt, iterations, 256);
+ SecretKeyFactory secretKeyFactory =
SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1");
+ SecretKey passwordKey = secretKeyFactory.generateSecret(passwordKeySpec);
+ passwordKeySpec.clearPassword();
+ byte[] encoded = passwordKey.getEncoded();
+ return Base64.encodeBytes(new SecretKeySpec(encoded,
"AES").getEncoded());
+ }
+
public static PasswordHash instance()
{
return (PasswordHash) Component.getInstance(PasswordHash.class,
ScopeType.STATELESS);
}
+
+ public int getSaltLength()
+ {
+ return saltLength;
+ }
+
+ public void setSaltLength(int saltLength)
+ {
+ this.saltLength = saltLength;
+ }
}