diff --git a/ccm-core/pom.xml b/ccm-core/pom.xml
index 67c2b3c10..3f12eee07 100644
--- a/ccm-core/pom.xml
+++ b/ccm-core/pom.xml
@@ -70,6 +70,10 @@
commons-beanutilscommons-beanutils
+
+ commons-codec
+ commons-codec
+ oro
@@ -262,7 +266,22 @@
-
+
+ org.jboss.tattletale
+ tattletale-maven
+
+
+
+ report
+
+
+
+
+ ${project.build.directory}
+ ${project.build.directory}/tattletale
+
+
+
diff --git a/ccm-core/src/main/java/org/libreccm/core/AbstractEntityRepository.java b/ccm-core/src/main/java/org/libreccm/core/AbstractEntityRepository.java
index 888474f6b..b7a782d3f 100644
--- a/ccm-core/src/main/java/org/libreccm/core/AbstractEntityRepository.java
+++ b/ccm-core/src/main/java/org/libreccm/core/AbstractEntityRepository.java
@@ -18,7 +18,6 @@
*/
package org.libreccm.core;
-import org.hibernate.validator.cdi.HibernateValidator;
import java.util.List;
@@ -28,7 +27,6 @@ import javax.persistence.TypedQuery;
import javax.persistence.criteria.CriteriaBuilder;
import javax.persistence.criteria.CriteriaQuery;
import javax.persistence.criteria.Root;
-import javax.validation.Validator;
/**
* A base class providing common method needed by every repository.
diff --git a/ccm-core/src/main/java/org/libreccm/core/CoreConstants.java b/ccm-core/src/main/java/org/libreccm/core/CoreConstants.java
index 1ea2d4bf0..d88880828 100644
--- a/ccm-core/src/main/java/org/libreccm/core/CoreConstants.java
+++ b/ccm-core/src/main/java/org/libreccm/core/CoreConstants.java
@@ -26,6 +26,7 @@ package org.libreccm.core;
public final class CoreConstants {
public static final String CORE_XML_NS = "http://core.libreccm.org";
+ public static final String UTF8 = "UTF-8";
private CoreConstants() {
//Nothing
diff --git a/ccm-core/src/main/java/org/libreccm/core/PasswordHashingFailedException.java b/ccm-core/src/main/java/org/libreccm/core/PasswordHashingFailedException.java
new file mode 100644
index 000000000..0c686cbc9
--- /dev/null
+++ b/ccm-core/src/main/java/org/libreccm/core/PasswordHashingFailedException.java
@@ -0,0 +1,67 @@
+/*
+ * Copyright (C) 2015 LibreCCM Foundation.
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
+ * MA 02110-1301 USA
+ */
+
+package org.libreccm.core;
+
+/**
+ *
+ * @author Jens Pelzetter
+ */
+public class PasswordHashingFailedException extends RuntimeException {
+
+ private static final long serialVersionUID = 1L;
+
+ /**
+ * Creates a new instance of PasswordHashingFailed without detail message.
+ */
+ public PasswordHashingFailedException() {
+ super();
+ }
+
+
+ /**
+ * Constructs an instance of PasswordHashingFailed with the specified detail message.
+ *
+ * @param msg The detail message.
+ */
+ public PasswordHashingFailedException(final String msg) {
+ super(msg);
+ }
+
+ /**
+ * Constructs an instance of PasswordHashingFailed which wraps the
+ * specified exception.
+ *
+ * @param exception The exception to wrap.
+ */
+ public PasswordHashingFailedException(final Exception exception) {
+ super(exception);
+ }
+
+ /**
+ * Constructs an instance of PasswordHashingFailed with the specified message which also wraps the
+ * specified exception.
+ *
+ * @param msg The detail message.
+ * @param exception The exception to wrap.
+ */
+ public PasswordHashingFailedException(final String msg, final Exception exception) {
+ super(msg, exception);
+ }
+}
diff --git a/ccm-core/src/main/java/org/libreccm/core/UserManager.java b/ccm-core/src/main/java/org/libreccm/core/UserManager.java
index 3253952fd..2b506f306 100644
--- a/ccm-core/src/main/java/org/libreccm/core/UserManager.java
+++ b/ccm-core/src/main/java/org/libreccm/core/UserManager.java
@@ -18,6 +18,9 @@
*/
package org.libreccm.core;
+import static org.libreccm.core.CoreConstants.*;
+
+import org.apache.commons.codec.binary.Base64;
import java.io.UnsupportedEncodingException;
import java.security.MessageDigest;
@@ -30,7 +33,7 @@ import javax.inject.Inject;
/**
* This class provides complex operations on {@link User} objects like updating
* the password. To use this class add an injection point to your class.
- *
+ *
* @author Jens Pelzetter
*/
@RequestScoped
@@ -39,74 +42,134 @@ public class UserManager {
/**
* {@link UserRepository} for interacting with the database. The method
* takes care of hashing the password with random salt.
- *
+ *
*/
@Inject
private transient UserRepository userRepository;
-
+
+ private byte[] generateHash(final byte[] password, final byte[] salt) {
+ final byte[] saltedPassword = new byte[password.length + salt.length];
+
+ System.arraycopy(password, 0, saltedPassword, 0, password.length);
+ System.arraycopy(salt, 0, saltedPassword, password.length, salt.length);
+
+ try {
+ final MessageDigest digest = MessageDigest.getInstance(
+ getHashAlgorithm());
+
+ final byte[] hashedPassword = digest.digest(saltedPassword);
+
+ return hashedPassword;
+ } catch (NoSuchAlgorithmException ex) {
+ throw new PasswordHashingFailedException(
+ "Failed to generate hash for password", ex);
+ }
+ }
+
/**
* Update the password of an user.
- *
- * @param user The user whose password is to be updated.
+ *
+ * @param user The user whose password is to be updated.
* @param password The new password.
*/
public void updatePassword(final User user, final String password) {
try {
final Random random = new Random(System.currentTimeMillis());
- final byte[] passwordBytes = password.getBytes("UTF-8");
+ final byte[] passwordBytes = password.getBytes(UTF8);
final byte[] salt = new byte[getSaltLength()];
random.nextBytes(salt);
- final byte[] saltedPassword = new byte[passwordBytes.length
- + salt.length];
- System.arraycopy(passwordBytes,
- 0,
- saltedPassword,
- 0,
- passwordBytes.length);
- System.arraycopy(salt,
- 0,
- saltedPassword,
- passwordBytes.length,
- salt.length);
- final MessageDigest digest = MessageDigest.getInstance(
- getHashAlgorithm());
- final byte[] hashedBytes = digest.digest(saltedPassword);
-
- final String hashedPassword = new String(hashedBytes, "UTF-8");
+ final byte[] hashedBytes = generateHash(passwordBytes, salt);
+
+ final Base64 base64 = new Base64();
+ final String hashedPassword = base64.encodeToString(hashedBytes);
+ final String saltStr = base64.encodeToString(salt);
user.setPassword(hashedPassword);
+ user.setSalt(saltStr);
userRepository.save(user);
-
- } catch (NoSuchAlgorithmException ex) {
- throw new RuntimeException(String.format(
- "Configured hash algorithm '%s 'is not available.",
- getHashAlgorithm()), ex);
+
} catch (UnsupportedEncodingException ex) {
- throw new RuntimeException("UTF-8 charset is not supported.");
+ throw new PasswordHashingFailedException(
+ "UTF-8 charset is not supported.", ex);
+ }
+ }
+
+ /**
+ * Verify a password for the a specific user.
+ *
+ * @param user The user whose password is to be checked.
+ * @param password The password to verify.
+ *
+ * @return {@code true} if the provided password matches the password
+ * stored, {@code false} if not.
+ */
+ public boolean verifyPasswordForUser(final User user,
+ final String password) {
+ final Base64 base64 = new Base64();
+
+ try {
+ final byte[] hashed = generateHash(
+ password.getBytes(UTF8), base64.decode(user.getSalt()));
+
+ final String hashedPassword = base64.encodeAsString(hashed);
+
+ return hashedPassword.equals(user.getPassword());
+
+ } catch (UnsupportedEncodingException ex) {
+ throw new PasswordHashingFailedException(
+ "Failed to generate hash of password", ex);
+ }
+ }
+
+ public boolean verifyPasswordForScreenname(final String screenname,
+ final String password)
+ throws UserNotFoundException {
+
+ final User user = userRepository.findByScreenName(screenname);
+
+ if (user == null) {
+ throw new UserNotFoundException(String.format(
+ "No user identified by screenname '%s' found.", screenname));
+ } else {
+ return verifyPasswordForUser(user, password);
+ }
+ }
+
+ public boolean verifyPasswordForEmail(final String emailAddress,
+ final String password)
+ throws UserNotFoundException{
+
+ final User user = userRepository.findByEmailAddress(emailAddress);
+
+ if (user == null) {
+ throw new UserNotFoundException(String.format(
+ "No user identified by email address '%s' found.", emailAddress));
+ } else {
+ return verifyPasswordForUser(user, password);
}
}
/**
* Gets the hash algorithm to use.
- *
+ *
* ToDo: Make configurable.
- *
+ *
* @return At the moment SHA-512, will be made configurable.
*/
- private String getHashAlgorithm() {
+ public String getHashAlgorithm() {
return "SHA-512";
}
/**
* Returns the length for the salt (number of bytes).
- *
+ *
* ToDo: Make configurable.
- *
+ *
* @return At the moment 256. Will be made configurable.
*/
- private int getSaltLength() {
+ public int getSaltLength() {
return 256;
}
diff --git a/ccm-core/src/main/java/org/libreccm/core/UserNotFoundException.java b/ccm-core/src/main/java/org/libreccm/core/UserNotFoundException.java
new file mode 100644
index 000000000..6aa986d47
--- /dev/null
+++ b/ccm-core/src/main/java/org/libreccm/core/UserNotFoundException.java
@@ -0,0 +1,67 @@
+/*
+ * Copyright (C) 2015 LibreCCM Foundation.
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
+ * MA 02110-1301 USA
+ */
+
+package org.libreccm.core;
+
+/**
+ *
+ * @author Jens Pelzetter
+ */
+public class UserNotFoundException extends Exception {
+
+ private static final long serialVersionUID = 1L;
+
+ /**
+ * Creates a new instance of UserNotFoundException without detail message.
+ */
+ public UserNotFoundException() {
+ super();
+ }
+
+
+ /**
+ * Constructs an instance of UserNotFoundException with the specified detail message.
+ *
+ * @param msg The detail message.
+ */
+ public UserNotFoundException(final String msg) {
+ super(msg);
+ }
+
+ /**
+ * Constructs an instance of UserNotFoundException which wraps the
+ * specified exception.
+ *
+ * @param exception The exception to wrap.
+ */
+ public UserNotFoundException(final Exception exception) {
+ super(exception);
+ }
+
+ /**
+ * Constructs an instance of UserNotFoundException with the specified message which also wraps the
+ * specified exception.
+ *
+ * @param msg The detail message.
+ * @param exception The exception to wrap.
+ */
+ public UserNotFoundException(final String msg, final Exception exception) {
+ super(msg, exception);
+ }
+}
diff --git a/ccm-core/src/test/java/org/libreccm/core/UserManagerTest.java b/ccm-core/src/test/java/org/libreccm/core/UserManagerTest.java
new file mode 100644
index 000000000..e677b6612
--- /dev/null
+++ b/ccm-core/src/test/java/org/libreccm/core/UserManagerTest.java
@@ -0,0 +1,288 @@
+/*
+ * Copyright (C) 2015 LibreCCM Foundation.
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
+ * MA 02110-1301 USA
+ */
+package org.libreccm.core;
+
+import static org.hamcrest.Matchers.*;
+
+import org.jboss.arquillian.container.test.api.Deployment;
+import org.jboss.arquillian.junit.Arquillian;
+import org.jboss.arquillian.junit.InSequence;
+import org.jboss.arquillian.persistence.PersistenceTest;
+import org.jboss.arquillian.persistence.UsingDataSet;
+import org.jboss.arquillian.transaction.api.annotation.TransactionMode;
+import org.jboss.arquillian.transaction.api.annotation.Transactional;
+import org.jboss.shrinkwrap.api.ShrinkWrap;
+import org.jboss.shrinkwrap.api.asset.EmptyAsset;
+import org.jboss.shrinkwrap.api.spec.WebArchive;
+import org.jboss.shrinkwrap.resolver.api.maven.Maven;
+import org.jboss.shrinkwrap.resolver.api.maven.PomEquippedResolveStage;
+import org.junit.After;
+import org.junit.AfterClass;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.junit.experimental.categories.Category;
+import org.junit.runner.RunWith;
+
+import static org.libreccm.core.CoreConstants.*;
+
+import org.apache.commons.codec.binary.Base64;
+import org.jboss.arquillian.container.test.api.ShouldThrowException;
+import org.libreccm.tests.categories.IntegrationTest;
+
+import java.io.File;
+import java.io.UnsupportedEncodingException;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+
+import javax.inject.Inject;
+import javax.persistence.EntityManager;
+import javax.persistence.PersistenceContext;
+
+import static org.junit.Assert.*;
+
+/**
+ *
+ * @author Jens Pelzetter
+ */
+@Category(IntegrationTest.class)
+@RunWith(Arquillian.class)
+@PersistenceTest
+@Transactional(TransactionMode.COMMIT)
+public class UserManagerTest {
+
+ @Inject
+ private transient UserRepository userRepository;
+
+ @Inject
+ private transient UserManager userManager;
+
+ @PersistenceContext
+ private transient EntityManager entityManager;
+
+ public UserManagerTest() {
+ }
+
+ @BeforeClass
+ public static void setUpClass() {
+ }
+
+ @AfterClass
+ public static void tearDownClass() {
+ }
+
+ @Before
+ public void setUp() {
+ }
+
+ @After
+ public void tearDown() {
+ }
+
+ @Deployment
+ public static WebArchive createDeployment() {
+ final PomEquippedResolveStage pom = Maven
+ .resolver()
+ .loadPomFromFile("pom.xml");
+ final PomEquippedResolveStage dependencies = pom.
+ importCompileAndRuntimeDependencies();
+ final File[] libs = dependencies.resolve().withTransitivity().asFile();
+
+ for (File lib : libs) {
+ System.err.printf("Adding file '%s' to test archive...%n",
+ lib.getName());
+ }
+
+ return ShrinkWrap
+ .create(WebArchive.class,
+ "LibreCCM-org.libreccm.core.UserRepositoryTest.war")
+ .addPackage(User.class.getPackage())
+ .addPackage(org.libreccm.web.Application.class.getPackage())
+ .addPackage(org.libreccm.categorization.Category.class.
+ getPackage())
+ .addPackage(org.libreccm.l10n.LocalizedString.class.getPackage()).
+ addPackage(org.libreccm.jpa.EntityManagerProducer.class
+ .getPackage())
+ .addPackage(org.libreccm.jpa.utils.MimeTypeConverter.class
+ .getPackage())
+ .addPackage(org.libreccm.testutils.EqualsVerifier.class.
+ getPackage())
+ .addPackage(org.libreccm.tests.categories.IntegrationTest.class
+ .getPackage())
+ .addAsLibraries(libs)
+ .addAsResource("test-persistence.xml",
+ "META-INF/persistence.xml")
+ .addAsWebInfResource("test-web.xml", "WEB-INF/web.xml")
+ .addAsWebInfResource(EmptyAsset.INSTANCE, "WEB-INF/beans.xml");
+
+ }
+
+ @Test
+ @InSequence(10)
+ public void userRepoIsInjected() {
+ assertThat(userRepository, is(not(nullValue())));
+ }
+
+ @Test
+ @InSequence(20)
+ public void userManagerIsInjected() {
+ assertThat(userManager, is(not(nullValue())));
+ }
+
+ @Test
+ @InSequence(30)
+ public void entityManagerIsInjected() {
+ assertThat(entityManager, is(not(nullValue())));
+ }
+
+ @Test
+ @UsingDataSet("datasets/org/libreccm/core/UserRepositoryTest/data.json")
+ @InSequence(100)
+ public void updatePassword() throws NoSuchAlgorithmException,
+ UnsupportedEncodingException {
+
+ final User jdoe = userRepository.findById(-10L);
+
+ assertThat(jdoe, is(not(nullValue())));
+ assertThat(jdoe.getScreenName(), is("jdoe"));
+
+ final String newPassword = "foobar";
+
+ userManager.updatePassword(jdoe, newPassword);
+
+ final Base64 base64 = new Base64();
+ final User user = entityManager.find(User.class, -10L);
+ final byte[] passwordBytes = newPassword.getBytes(UTF8);
+ final String salt = user.getSalt();
+ final byte[] saltBytes = base64.decode(salt);
+
+ assertThat(saltBytes.length, is(userManager.getSaltLength()));
+
+ final MessageDigest digest = MessageDigest.getInstance(userManager
+ .getHashAlgorithm());
+
+ final byte[] saltedPassword = new byte[passwordBytes.length
+ + saltBytes.length];
+ System.arraycopy(passwordBytes,
+ 0,
+ saltedPassword,
+ 0,
+ passwordBytes.length);
+ System.arraycopy(saltBytes,
+ 0,
+ saltedPassword,
+ passwordBytes.length,
+ saltBytes.length);
+ final byte[] hashedBytes = digest.digest(saltedPassword);
+
+ final String hashed = base64.encodeToString(hashedBytes);
+ assertThat(user.getPassword(), is(equalTo(hashed)));
+ }
+
+ @Test
+ @UsingDataSet(
+ "datasets/org/libreccm/core/UserManagerTest/verify-password.json")
+ @InSequence(200)
+ public void verifyPasswordForUser() {
+ final User user = userRepository.findById(-10L);
+
+ //userManager.updatePassword(user, "foobar");
+
+ final boolean result = userManager.verifyPasswordForUser(user, "foobar");
+
+ assertThat(result, is(true));
+ }
+
+ @Test
+ @UsingDataSet(
+ "datasets/org/libreccm/core/UserManagerTest/verify-password.json")
+ @InSequence(300)
+ public void verifyPasswordForScreenname() throws UserNotFoundException {
+ final boolean result = userManager.verifyPasswordForScreenname(
+ "jdoe", "foobar");
+
+ assertThat(result, is(true));
+ }
+
+ @Test
+ @UsingDataSet(
+ "datasets/org/libreccm/core/UserManagerTest/verify-password.json")
+ @InSequence(400)
+ public void verifyPasswordForEmail() throws UserNotFoundException {
+ final boolean result = userManager.verifyPasswordForEmail(
+ "john.doe@example.com", "foobar");
+
+ assertThat(result, is(true));
+ }
+
+ @Test
+ @UsingDataSet(
+ "datasets/org/libreccm/core/UserManagerTest/verify-password.json")
+ @InSequence(500)
+ public void verifyPasswordForUserWrongPassword() {
+ final User user = userRepository.findById(-10L);
+
+ final boolean result = userManager.verifyPasswordForUser(user, "wrong");
+
+ assertThat(result, is(false));
+ }
+
+ @Test
+ @UsingDataSet(
+ "datasets/org/libreccm/core/UserManagerTest/verify-password.json")
+ @InSequence(600)
+ public void verifyPasswordForScreennameWrongPassword() throws
+ UserNotFoundException {
+ final boolean result = userManager.verifyPasswordForScreenname(
+ "jdoe", "wrong");
+
+ assertThat(result, is(false));
+ }
+
+ @Test
+ @UsingDataSet(
+ "datasets/org/libreccm/core/UserManagerTest/verify-password.json")
+ @InSequence(400)
+ public void verifyPasswordForEmailWrongPassword() throws
+ UserNotFoundException {
+ final boolean result = userManager.verifyPasswordForEmail(
+ "john.doe@example.com", "wrong");
+
+ assertThat(result, is(false));
+ }
+
+ @Test(expected = UserNotFoundException.class)
+ @ShouldThrowException(UserNotFoundException.class)
+ @UsingDataSet(
+ "datasets/org/libreccm/core/UserManagerTest/verify-password.json")
+ @InSequence(700)
+ public void verifyPasswordForScreennameNoUser() throws UserNotFoundException {
+ userManager.verifyPasswordForScreenname("nobody", "foobar");
+ }
+
+ @Test(expected = UserNotFoundException.class)
+ @ShouldThrowException(UserNotFoundException.class)
+ @UsingDataSet(
+ "datasets/org/libreccm/core/UserManagerTest/verify-password.json")
+ @InSequence(800)
+ public void verifyPasswordForEmailNoUser() throws UserNotFoundException {
+ userManager.verifyPasswordForEmail("nobody@example.com", "foobar");
+ }
+
+}
diff --git a/ccm-core/src/test/resources/datasets/org/libreccm/core/UserManagerTest/verify-password.json b/ccm-core/src/test/resources/datasets/org/libreccm/core/UserManagerTest/verify-password.json
new file mode 100644
index 000000000..76d270f06
--- /dev/null
+++ b/ccm-core/src/test/resources/datasets/org/libreccm/core/UserManagerTest/verify-password.json
@@ -0,0 +1,31 @@
+{
+ "subjects":
+ [
+ {
+ "subject_id": -10
+ }
+ ],
+ "subject_email_addresses":
+ [
+ {
+ "subject_id": -10,
+ "email_address": "john.doe@example.com",
+ "bouncing": false,
+ "verified": true
+ }
+ ],
+ "ccm_users":
+ [
+ {
+ "banned": false,
+ "hash_algorithm": "SHA-512",
+ "family_name": "Doe",
+ "given_name": "John",
+ "password": "C+o2w6mp+eLrbluMEgKMVSdP50A9BMethXN8R3yihtkbzt7WfWsde2nmq/t5gq6im3J8i3jw4Y3YrKHou8JQ2A==",
+ "password_reset_required": false,
+ "salt": "Fu8FPgqAal4GZp1hDjkOB+t6ITRCcO7HBoN5Xqf29UnVj5NUdUFZRTyKYMBEx6JmZGmHcMDG9OGVCKcEM9oyScSRreJs4B51wM44NM6KeRwbCf+VhBn14DkBrl40ygraNf+AJacKpMyCpFI0O/Am7mMDWL4flskBsylkxaQn3vKfzgN5MVG2szW//I6Q6YEH9AuL8LauS6fKaVynMzzu3xzD8Hjqvvlnzym898eom2lqScPfg5g4e8Ww13HCHAYe6twupAW/BjUNax5HSioEisZN/P1UGrde8uFEj+hbbavrWYZuilPuEu25+/98jyXx6542agqrWN8j0SFYcIyOgA==",
+ "screen_name": "jdoe",
+ "subject_id": -10
+ }
+ ]
+}
\ No newline at end of file