Skip to content

Commit

Permalink
For #759 - started implementing support for 'traccar manager' app - a…
Browse files Browse the repository at this point in the history
…dded new password hashing method and user's salt field
  • Loading branch information
vitalidze committed Dec 11, 2016
1 parent 9adc2b2 commit 0147f42
Show file tree
Hide file tree
Showing 6 changed files with 129 additions and 88 deletions.
13 changes: 12 additions & 1 deletion src/main/java/org/traccar/web/server/model/DBMigrations.java
Expand Up @@ -66,7 +66,8 @@ public void migrate(EntityManager em) throws Exception {
new SetReportsFilterAndPreview(),
new SetDefaultExpiredFlagForEvents(),
new SetDefaultMatchServiceSettings(),
new RemoveMapQuest()
new RemoveMapQuest(),
new SetUserHashSalt()
}) {
em.getTransaction().begin();
try {
Expand Down Expand Up @@ -491,4 +492,14 @@ public void migrate(EntityManager em) throws Exception {
.executeUpdate();
}
}

static class SetUserHashSalt implements Migration {
@Override
public void migrate(EntityManager em) throws Exception {
for (User user : em.createQuery("SELECT x FROM " + User.class.getName() + " x WHERE x.salt IS NULL", User.class)
.getResultList()) {
user.setSalt(PasswordUtils.generateRandomUserSalt());
}
}
}
}
47 changes: 29 additions & 18 deletions src/main/java/org/traccar/web/server/model/DataServiceImpl.java
Expand Up @@ -15,6 +15,9 @@
*/
package org.traccar.web.server.model;

import static org.traccar.web.server.model.PasswordUtils.generateRandomUserSalt;
import static org.traccar.web.server.model.PasswordUtils.hash;

import java.io.Reader;
import java.io.StringReader;
import java.lang.reflect.InvocationTargetException;
Expand Down Expand Up @@ -127,11 +130,11 @@ public User login(String login, String password, boolean passwordHashed) throws
throw new IllegalStateException();
}
} else {
if (!storedPassword.equals(user.getPasswordHashMethod().doHash(password, getApplicationSettings().getSalt()))) {
if (!storedPassword.equals(hash(user.getPasswordHashMethod(), password, getApplicationSettings().getSalt(), user.getSalt()))) {
// check for the old implementation without salt
// if it matches then update password with new salt
if (storedPassword.equals(user.getPasswordHashMethod().doHash(password, ""))) {
user.setPassword(user.getPasswordHashMethod().doHash(password, getApplicationSettings().getSalt()));
if (storedPassword.equals(hash(user.getPasswordHashMethod(), password, "", ""))) {
user.setPassword(hash(user.getPasswordHashMethod(), password, getApplicationSettings().getSalt(), user.getSalt()));
} else {
throw new IllegalStateException();
}
Expand All @@ -151,7 +154,7 @@ public User login(String login, String password, boolean passwordHashed) throws
*/
if (!user.getPasswordHashMethod().equals(getApplicationSettings().getDefaultHashImplementation()) && !passwordHashed) {
user.setPasswordHashMethod(getApplicationSettings().getDefaultHashImplementation());
user.setPassword(user.getPasswordHashMethod().doHash(password, getApplicationSettings().getSalt()));
user.setPassword(hash(user.getPasswordHashMethod(), password, getApplicationSettings().getSalt(), user.getSalt()));
}

setSessionUser(user);
Expand Down Expand Up @@ -181,16 +184,17 @@ public User register(String login, String password) throws AccessDeniedException
query.setParameter("login", login);
List<User> results = query.getResultList();
if (results.isEmpty()) {
User user = new User();
user.setLogin(login);
user.setPasswordHashMethod(getApplicationSettings().getDefaultHashImplementation());
user.setPassword(user.getPasswordHashMethod().doHash(password, getApplicationSettings().getSalt()));
user.setManager(Boolean.TRUE); // registered users are always managers
user.setUserSettings(getUserSettingsForNewUser());
getSessionEntityManager().persist(user);
getSessionEntityManager().persist(UIStateEntry.createDefaultArchiveGridStateEntry(user));
setSessionUser(user);
return fillUserSettings(new User(user));
User user = new User();
user.setLogin(login);
user.setPasswordHashMethod(getApplicationSettings().getDefaultHashImplementation());
user.setSalt(generateRandomUserSalt());
user.setPassword(hash(user.getPasswordHashMethod(), password, getApplicationSettings().getSalt(), user.getSalt()));
user.setManager(Boolean.TRUE); // registered users are always managers
user.setUserSettings(getUserSettingsForNewUser());
getSessionEntityManager().persist(user);
getSessionEntityManager().persist(UIStateEntry.createDefaultArchiveGridStateEntry(user));
setSessionUser(user);
return fillUserSettings(new User(user));
}
else
{
Expand Down Expand Up @@ -241,7 +245,8 @@ public User addUser(User user) throws InvalidMaxDeviceNumberForUserException {
user.setManagedBy(currentUser);
validateMaximumNumberOfDevices(user, null, user.getMaxNumOfDevices());
user.setPasswordHashMethod(getApplicationSettings().getDefaultHashImplementation());
user.setPassword(user.getPasswordHashMethod().doHash(user.getPassword(), getApplicationSettings().getSalt()));
user.setSalt(generateRandomUserSalt());
user.setPassword(hash(user.getPasswordHashMethod(), user.getPassword(), getApplicationSettings().getSalt(), user.getSalt()));
if (user.getUserSettings() == null) {
user.setUserSettings(getUserSettingsForNewUser());
}
Expand Down Expand Up @@ -284,7 +289,10 @@ public User updateUser(User user) throws AccessDeniedException {
|| currentUser.getPasswordHashMethod().equals(PasswordHashMethod.PLAIN)
&& !getApplicationSettings().getDefaultHashImplementation().equals(PasswordHashMethod.PLAIN)) {
currentUser.setPasswordHashMethod(getApplicationSettings().getDefaultHashImplementation());
currentUser.setPassword(currentUser.getPasswordHashMethod().doHash(user.getPassword(), getApplicationSettings().getSalt()));
if (currentUser.getSalt() == null) {
currentUser.setSalt(generateRandomUserSalt());
}
currentUser.setPassword(hash(currentUser.getPasswordHashMethod(), user.getPassword(), getApplicationSettings().getSalt(), currentUser.getSalt()));
}
if (currentUser.getAdmin() || currentUser.getManager())
{
Expand All @@ -307,12 +315,15 @@ public User updateUser(User user) throws AccessDeniedException {
// update password
if (currentUser.getAdmin() || currentUser.getManager()) {
User existingUser = entityManager.find(User.class, user.getId());
if (existingUser.getSalt() == null) {
existingUser.setSalt(generateRandomUserSalt());
}
// Checks if password has changed or default hash method not equal to current user hash method
if (!existingUser.getPassword().equals(user.getPassword())
&& !existingUser.getPassword().equals(existingUser.getPasswordHashMethod().doHash(user.getPassword(), getApplicationSettings().getSalt()))
&& !existingUser.getPassword().equals(hash(existingUser.getPasswordHashMethod(), user.getPassword(), getApplicationSettings().getSalt(), existingUser.getSalt()))
|| !existingUser.getPasswordHashMethod().equals(getApplicationSettings().getDefaultHashImplementation())) {
existingUser.setPasswordHashMethod(getApplicationSettings().getDefaultHashImplementation());
existingUser.setPassword(existingUser.getPasswordHashMethod().doHash(user.getPassword(), getApplicationSettings().getSalt()));
existingUser.setPassword(hash(existingUser.getPasswordHashMethod(), user.getPassword(), getApplicationSettings().getSalt(), existingUser.getSalt()));
}
entityManager.merge(existingUser);
} else {
Expand Down
66 changes: 63 additions & 3 deletions src/main/java/org/traccar/web/server/model/PasswordUtils.java
Expand Up @@ -15,20 +15,28 @@
*/
package org.traccar.web.server.model;

import org.traccar.web.shared.model.PasswordHashMethod;

import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.PBEKeySpec;
import javax.xml.bind.DatatypeConverter;
import java.security.GeneralSecurityException;
import java.security.MessageDigest;
import java.security.SecureRandom;
import java.util.Random;

public class PasswordUtils {
class PasswordUtils {
private static final Random RANDOM = new SecureRandom();
private static final int SALT_SIZE = 24;

private PasswordUtils() {
}

public static String generateRandomString() {
static String generateRandomString() {
return generateRandomString(8);
}

public static String generateRandomString(int length) {
static String generateRandomString(int length) {

StringBuilder sb = new StringBuilder(length);
for (int i = 0; i < length; i++) {
Expand All @@ -43,4 +51,56 @@ public static String generateRandomString(int length) {
}
return sb.toString();
}

static String generateRandomUserSalt() {
byte[] salt = new byte[SALT_SIZE];
RANDOM.nextBytes(salt);
return DatatypeConverter.printHexBinary(salt);
}

static String hash(PasswordHashMethod method, String password, String appSalt, String userSalt) {
try {
switch (method) {
case SHA512:
return sha512(password, appSalt);
case MD5:
return md5(password, appSalt);
case PBKDF2WithHmacSha1:
return pbkdf2WithHmacSha1(password, userSalt);
default:
return password;
}
} catch (GeneralSecurityException gse) {
throw new RuntimeException(gse);
}
}

private static String sha512(String password, String salt) throws GeneralSecurityException {
final MessageDigest sha512 = MessageDigest.getInstance("SHA-512");
sha512.reset();
if (salt != null && !salt.isEmpty()) {
sha512.update(salt.getBytes());
}
byte[] data = sha512.digest(password.getBytes());
return DatatypeConverter.printHexBinary(data).toLowerCase();
}

private static String md5(String s, String salt) throws GeneralSecurityException {
final MessageDigest md5 = MessageDigest.getInstance("MD5");
md5.reset();
if (salt != null && !salt.isEmpty()) {
md5.update(salt.getBytes());
}
byte[] data = md5.digest(s.getBytes());
return DatatypeConverter.printHexBinary(data).toLowerCase();
}

private static String pbkdf2WithHmacSha1(String s, String salt) throws GeneralSecurityException {
final int ITERATIONS = 1000;
final int HASH_SIZE = 24;

PBEKeySpec spec = new PBEKeySpec(s.toCharArray(), DatatypeConverter.parseHexBinary(salt), ITERATIONS, HASH_SIZE * Byte.SIZE);
SecretKeyFactory factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1");
return DatatypeConverter.printHexBinary(factory.generateSecret(spec).getEncoded());
}
}
63 changes: 5 additions & 58 deletions src/main/java/org/traccar/web/shared/model/PasswordHashMethod.java
Expand Up @@ -17,66 +17,13 @@

import com.google.gwt.user.client.rpc.IsSerializable;

import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;

public enum PasswordHashMethod implements IsSerializable {
PLAIN("plain") {
@Override
public String doHash(String s, String salt) {
return s;
}
},
SHA512("sha512") {
@Override
public String doHash(String s, String salt) {
try {
final MessageDigest sha512 = MessageDigest.getInstance("SHA-512");
sha512.reset();
if (salt != null && !salt.isEmpty()) {
sha512.update(salt.getBytes());
}
byte[] data = sha512.digest(s.getBytes());
StringBuilder hexData = new StringBuilder();
for (int byteIndex = 0; byteIndex < data.length; byteIndex++) {
hexData.append(Integer.toString((data[byteIndex] & 0xff) + 0x100, 16).substring(1));
}
return hexData.toString();
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException(e);
}
}
},
MD5("md5") {
@Override
public String doHash(String s, String salt) {
try {
final MessageDigest md5 = MessageDigest.getInstance("MD5");
md5.reset();
if (salt != null && !salt.isEmpty()) {
md5.update(salt.getBytes());
}
byte[] array = md5.digest(s.getBytes());
StringBuilder hexData = new StringBuilder();
for (int i = 0; i < array.length; ++i) {
hexData.append(Integer.toHexString((array[i] & 0xFF) | 0x100).substring(1, 3));
}
return hexData.toString();
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException(e);
}
}
};

final String method;

PasswordHashMethod(String name) {
this.method = name;
}
PLAIN,
SHA512,
MD5,
PBKDF2WithHmacSha1;

public String getName() {
return method;
return name().toLowerCase();
}

public abstract String doHash(String s, String salt);
}
11 changes: 11 additions & 0 deletions src/main/java/org/traccar/web/shared/model/User.java
Expand Up @@ -117,6 +117,17 @@ public PasswordHashMethod getPasswordHashMethod() {
return (password_hash_method == null) ? PasswordHashMethod.PLAIN : password_hash_method;
}

@JsonIgnore
private String salt;

public String getSalt() {
return salt;
}

public void setSalt(String salt) {
this.salt = salt;
}

// TODO temporary nullable to migrate from old database
private Boolean admin;

Expand Down
17 changes: 9 additions & 8 deletions src/test/java/org/traccar/web/server/model/DataServiceTest.java
Expand Up @@ -18,6 +18,7 @@
import static org.junit.Assert.*;
import static org.mockito.Mockito.*;

import static org.traccar.web.server.model.PasswordUtils.*;
import static org.traccar.web.shared.model.PasswordHashMethod.*;

import com.google.inject.AbstractModule;
Expand Down Expand Up @@ -214,33 +215,33 @@ public void testLoginPasswordHashAndSalt() throws Exception {
String salt = dataService.getApplicationSettings().getSalt();
// ordinary log in
User admin = dataService.login("admin", "admin");
assertEquals(MD5.doHash("admin", salt), admin.getPassword());
assertEquals(hash(MD5, "admin", salt, ""), admin.getPassword());
// log in with hash
admin = dataService.login("admin", MD5.doHash("admin", salt), true);
assertEquals(MD5.doHash("admin", salt), admin.getPassword());
admin = dataService.login("admin", hash(MD5, "admin", salt, null), true);
assertEquals(hash(MD5, "admin", salt, ""), admin.getPassword());
// update user
runInTransaction(new Callable<Object>() {
@Override
public Object call() throws Exception {
injector.getInstance(EntityManager.class).createQuery("UPDATE User u SET u.password=:pwd")
.setParameter("pwd", MD5.doHash("admin", null))
.setParameter("pwd", hash(MD5, "admin", null, null))
.executeUpdate();
return null;
}
});
// log in with hash will not be possible anymore
try {
admin = dataService.login("admin", MD5.doHash("admin", salt), true);
admin = dataService.login("admin", hash(MD5, "admin", salt, null), true);
fail("Should be impossible to log in with different hash");
} catch (IllegalStateException expected) {
// do nothing since exception is expected in this case
}
// check logging in with old hash (for backwards compatibility)
admin = dataService.login("admin", MD5.doHash("admin", null), true);
assertEquals(MD5.doHash("admin", null), admin.getPassword());
admin = dataService.login("admin", hash(MD5, "admin", null, null), true);
assertEquals(hash(MD5, "admin", null, null), admin.getPassword());
// log in and check if password is updated
admin = dataService.login("admin", "admin");
assertEquals(MD5.doHash("admin", salt), admin.getPassword());
assertEquals(hash(MD5, "admin", salt, null), admin.getPassword());
}

@Test
Expand Down

0 comments on commit 0147f42

Please sign in to comment.