/*
* jPOS Project [http://jpos.org]
* Copyright (C) 2000-2017 jPOS Software SRL
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program 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 Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.jpos.ee;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.util.Date;
import java.util.List;
import org.bouncycastle.util.Arrays;
import org.bouncycastle.util.encoders.Base64;
import org.hibernate.Criteria;
import org.hibernate.HibernateException;
import org.hibernate.criterion.Restrictions;
import org.jpos.iso.ISOUtil;
import org.jpos.security.SystemSeed;
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.PBEKeySpec;
/**
* @author Alejandro Revilla
*/
public class UserManager {
DB db;
VERSION version;
public UserManager (DB db) {
this (db, VERSION.ONE);
}
public UserManager (DB db, VERSION version) {
super ();
this.db = db;
this.version = version;
}
public void setPassword (User u, String clearpass) throws BLException {
setPassword(u, clearpass, null, version);
}
public void setPassword (User u, String clearpass, User author) throws BLException {
setPassword(u, clearpass, author, version);
}
public void setPassword (User u, String clearpass, User author, VERSION v) throws BLException {
if (u.getPasswordHash() != null)
u.addPasswordHistoryValue(u.getPasswordHash());
switch (v) {
case ZERO:
setV0Password (u, clearpass);
break;
case ONE:
setV1Password (u, clearpass);
break;
}
u.setPasswordChanged(new Date());
u.setForcePasswordChange(false);
RevisionManager revmgr = new RevisionManager(db);
if (author == null)
author = u;
revmgr.createRevision(author, "user." + u.getId(), "Password changed");
db.session().saveOrUpdate(u);
}
/**
* @return all users
* @throws HibernateException on low level hibernate related exception
*/
public List findAll () throws HibernateException {
return db.session().createCriteria (User.class)
.add (Restrictions.eq ("deleted", Boolean.FALSE))
.list();
}
public User getUserByNick (String nick)
throws HibernateException
{
return getUserByNick(nick, false);
}
public User getUserByNick (String nick, boolean includeDeleted)
throws HibernateException
{
Criteria crit = db.session().createCriteria(User.class)
.add(Restrictions.eq("nick", nick));
if (!includeDeleted)
crit = crit.add (Restrictions.eq ("deleted", Boolean.FALSE));
return (User) crit.uniqueResult();
}
/**
* @param nick name.
* @param pass hash
* @return the user
* @throws BLException if invalid user/pass
* @throws HibernateException on low level hibernate related exception
*/
public User getUserByNick (String nick, String pass)
throws HibernateException, BLException
{
User u = getUserByNick(nick);
assertNotNull (u, "User does not exist");
assertTrue(checkPassword(u, pass), "Invalid password");
return u;
}
public boolean checkPassword (User u, String clearpass)
throws HibernateException, BLException
{
assertNotNull(clearpass, "Invalid pass");
String passwordHash = u.getPasswordHash();
assertNotNull(passwordHash, "Password is null");
VERSION v = VERSION.getVersion(passwordHash);
assertTrue(v != VERSION.UNKNOWN, "Unknown password");
switch (v) {
case ZERO:
return checkV0Password(passwordHash, u.getId(), clearpass);
case ONE:
return checkV1Password(passwordHash, clearpass);
}
return false;
}
/**
* @param u the user
* @param clearpass new password in clear
* @return true if password is in PasswordHistory
*/
public boolean checkNewPassword (User u, String clearpass) throws BLException {
if (checkPassword (u, clearpass)) {
return false; // same password not allowed
}
for (PasswordHistory p : u.getPasswordhistory()) {
VERSION v = VERSION.getVersion(p.getValue());
switch (v) {
case ZERO:
if (checkV0Password(p.getValue(), u.getId(), clearpass))
return false;
case ONE:
if (checkV1Password (p.getValue(), clearpass))
return false;
}
}
return true;
}
public boolean upgradePassword (User u, String clearpass)
throws HibernateException, BLException
{
assertNotNull(clearpass, "Invalid pass");
String passwordHash = u.getPasswordHash();
assertNotNull(passwordHash, "Password is null");
VERSION v = VERSION.getVersion(passwordHash);
if (v == VERSION.ZERO && checkV0Password(passwordHash, u.getId(), clearpass)) {
setPassword(u, clearpass, null, VERSION.ONE);
return true;
}
return false;
}
public VERSION getVersion() {
return version;
}
public void setVersion(VERSION version) {
this.version = version;
}
private void setV1Password (User u, String clearpass) throws BLException {
assertNotNull(clearpass, "Invalid password");
byte[] salt = genSalt(VERSION.ONE.getSalt().length);
u.setPasswordHash(genV1Hash(clearpass, salt));
}
private String genV1Hash(String password, byte[] salt) throws BLException {
try {
SecretKeyFactory skf = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256");
int iterations = VERSION.ONE.getIterations();
PBEKeySpec spec = new PBEKeySpec(password.toCharArray(), salt, iterations, VERSION.ONE.getKeylength());
return org.bouncycastle.util.encoders.Base64.toBase64String(
Arrays.concatenate(
new byte[]{VERSION.ONE.getVersion()},
VERSION.ONE.getSalt(salt),
skf.generateSecret(spec).getEncoded())
);
} catch (Exception e) {
throw new BLException (e.getLocalizedMessage());
}
}
private boolean checkV1Password (String passwordHash, String clearpass) throws BLException {
byte[] b = Base64.decode(passwordHash);
byte[] salt = new byte[VERSION.ONE.getSalt().length];
System.arraycopy (b, 1, salt, 0, salt.length);
String computedPasswordHash = genV1Hash(clearpass, VERSION.ONE.getSalt(salt));
return computedPasswordHash.equals(passwordHash);
}
private boolean checkV0Password(String passwordHash, long id, String clearpass) {
return passwordHash.equals(genV0Hash(id, clearpass));
}
// HELPER METHODS
protected void assertNotNull (Object obj, String error) throws BLException {
if (obj == null)
throw new BLException (error);
}
protected void assertTrue (boolean condition, String error)
throws BLException
{
if (!condition)
throw new BLException (error);
}
private static String genV0Hash(long id, String clearpass) {
String hash = null;
try {
MessageDigest md = MessageDigest.getInstance ("SHA");
md.update (Long.toString(id,16).getBytes());
hash = ISOUtil.hexString (
md.digest (clearpass.getBytes())
).toLowerCase();
} catch (NoSuchAlgorithmException e) {
// should never happen
}
return hash;
}
private void setV0Password (User u, String clearpass) throws BLException {
assertNotNull(clearpass, "Invalid password");
u.setPasswordHash(genV0Hash(u.getId(), clearpass));
}
private byte[] genSalt(int len) {
SecureRandom sr;
try {
sr = SecureRandom.getInstance("SHA1PRNG");
byte[] salt = new byte[len];
sr.nextBytes(salt);
return salt;
} catch (NoSuchAlgorithmException ignored) {
// Should never happen, SHA1PRNG is a supported algorithm
}
return null;
}
public enum VERSION {
UNKNOWN((byte)0xFF, 0, 0, 0, null),
ZERO((byte)0, 0, 160, 40, null),
ONE((byte) 1,100000,2048, 388, Base64.decode("K7f2dgQQHK5CW6Wz+CscUA=="));
private byte version;
private int iterations;
private int keylength;
private int encodedLength;
private byte[] salt;
VERSION(byte version, int iterations, int keylength, int encodedLength, byte[] salt) {
this.version = version;
this.iterations = iterations;
this.keylength = keylength;
this.encodedLength = encodedLength;
this.salt = salt;
}
public byte getVersion() {
return version;
}
public int getIterations() {
return iterations;
}
public int getKeylength() {
return keylength;
}
public int getEncodedLength() {
return encodedLength;
}
public byte[] getSalt() {
return ISOUtil.xor(salt, SystemSeed.getSeed(salt.length, salt.length));
}
public byte[] getSalt(byte[] salt) {
return ISOUtil.xor(salt, getSalt());
}
public static VERSION getVersion (String hash) {
for (VERSION v : VERSION.values()) {
if (v.getEncodedLength() == hash.length())
return v;
if (v != UNKNOWN && v != ZERO) {
byte[] b = Base64.decode(hash);
if (b[0] == v.getVersion())
return v;
}
}
return UNKNOWN;
}
}
}