PasswordVerifier.java

/*
 * GovWay - A customizable API Gateway 
 * https://govway.org
 * 
 * Copyright (c) 2005-2024 Link.it srl (https://link.it). 
 * 
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License version 3, as published by
 * the Free Software Foundation.
 *
 * 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 General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 *
 */


package org.openspcoop2.utils.crypt;

import java.io.File;
import java.io.FileInputStream;
import java.io.InputStream;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Date;
import java.util.Enumeration;
import java.util.List;
import java.util.Properties;

import org.apache.commons.lang.StringUtils;
import org.openspcoop2.utils.Utilities;
import org.openspcoop2.utils.UtilsException;
import org.openspcoop2.utils.date.DateManager;
import org.openspcoop2.utils.regexp.RegularExpressionEngine;

/**
 * PasswordVerifier
 *
 * @author Andrea Poli (apoli@link.it)
 * @author $Author$
 * @version $Rev$, $Date$
 */

public class PasswordVerifier implements Serializable {
	
	private static final long serialVersionUID = 1L;
	
	private static final String PROPERTY_REGULAR_EXPRESSIONS_PREFIX = "passwordVerifier.regularExpression.";
	private static final String PROPERTY_LOGIN_CONTAINS = "passwordVerifier.notContainsLogin";
	private static final String PROPERTY_RESTRICTED_WORDS = "passwordVerifier.restrictedWords";
	private static final String PROPERTY_MIN_LENGTH = "passwordVerifier.minLength";
	private static final String PROPERTY_MAX_LENGTH = "passwordVerifier.maxLength";
	private static final String PROPERTY_INCLUDE_LOWER_CASE_LETTER = "passwordVerifier.lowerCaseLetter";
	private static final String PROPERTY_INCLUDE_UPPER_CASE_LETTER = "passwordVerifier.upperCaseLetter";
	private static final String PROPERTY_INCLUDE_NUMBER = "passwordVerifier.includeNumber";
	private static final String PROPERTY_INCLUDE_NOT_ALPHANUMERIC_SYMBOL = "passwordVerifier.includeNotAlphanumericSymbol";
	private static final String PROPERTY_ALL_DISTINCT_CHARACTERS = "passwordVerifier.allDistinctCharacters";
	private static final String PROPERTY_EXPIRE = "passwordVerifier.expireDays";
	private static final String PROPERTY_HISTORY = "passwordVerifier.history";
	
	protected List<String> regulaExpressions = new ArrayList<>();
	protected boolean notContainsLogin = false;
	protected List<String> restrictedWords = new ArrayList<>();
	protected int minLenght = -1;
	protected int maxLenght = -1;
	protected boolean includeLowerCaseLetter = false;
	protected boolean includeUpperCaseLetter = false;
	protected boolean includeNumber = false;
	protected boolean includeNotAlphanumericSymbol = false;
	protected boolean allDistinctCharacters = false;
	protected boolean checkPasswordExpire;
	protected int expireDays;
	protected boolean history;

	public PasswordVerifier(){}
	public PasswordVerifier(PasswordVerifier pv){
		this.regulaExpressions = pv.regulaExpressions;
		this.notContainsLogin = pv.notContainsLogin;
		this.restrictedWords = pv.restrictedWords;
		this.minLenght = pv.minLenght;
		this.maxLenght = pv.maxLenght;
		this.includeLowerCaseLetter = pv.includeLowerCaseLetter;
		this.includeUpperCaseLetter = pv.includeUpperCaseLetter;
		this.includeNumber = pv.includeNumber;
		this.includeNotAlphanumericSymbol = pv.includeNotAlphanumericSymbol;
		this.allDistinctCharacters = pv.allDistinctCharacters;
		this.checkPasswordExpire = pv.checkPasswordExpire;
		this.expireDays = pv.expireDays;
		this.history = pv.history;
	}
	public PasswordVerifier(String resource) throws UtilsException{
		this(resource, true);
	}
	public PasswordVerifier(String resource, boolean useDefaultIfNotFound) throws UtilsException{
		InputStream is = null;
		try{
			File f = new File(resource);
			if(f.exists()){
				is = new FileInputStream(f);
			}
			else{
				is = PasswordVerifier.class.getResourceAsStream(resource);
			}
			if(is==null) {
				is = PasswordVerifier.class.getResourceAsStream("/org/openspcoop2/utils/crypt/consolePassword.properties");
			}
			if(is!=null){
				Properties p = new Properties();
				p.load(is);
				this._init(p);
			}
			else{
				throw new Exception("Resource ["+resource+"] not found");
			}
		}catch(Exception e){
			throw new UtilsException(e.getMessage(),e);
		}finally{
			try{
				if(is!=null){
					is.close();
				}
			}catch(Exception eClose){
				// close
			}
		}
	}
	public PasswordVerifier(InputStream is) throws UtilsException{
		try{
			Properties p = new Properties();
			p.load(is);
			this._init(p);
		}catch(Exception e){
			throw new UtilsException(e.getMessage(),e);
		}
	}
	public PasswordVerifier(Properties p) throws UtilsException{
		this._init(p);
	}
	private void _init(Properties p) throws UtilsException{
		try{
			
			Properties tmpP = Utilities.readProperties(PROPERTY_REGULAR_EXPRESSIONS_PREFIX, p);
			if(tmpP!=null && tmpP.size()>0){
				Enumeration<?> en =tmpP.keys();
				while (en.hasMoreElements()) {
					String key = (String) en.nextElement();
					String value = tmpP.getProperty(key);
					this.regulaExpressions.add(value);
				}
			}
			
			String tmp = p.getProperty(PROPERTY_LOGIN_CONTAINS);
			if(tmp!=null){
				tmp=tmp.trim();
				try{
					this.notContainsLogin = Boolean.parseBoolean(tmp);
				}catch(Exception e){
					throw new Exception("Property '"+PROPERTY_LOGIN_CONTAINS+"' with wrong value '"+tmp+"': "+e.getMessage(),e);
				}
			}
			
			tmp = p.getProperty(PROPERTY_RESTRICTED_WORDS);
			if(tmp!=null){
				tmp=tmp.trim();
				if(tmp.contains(",")){
					String [] split = tmp.split(",");
					for (int i = 0; i < split.length; i++) {
						this.restrictedWords.add(split[i].trim());
					}
				}
				else{
					this.restrictedWords.add(tmp);
				}
			}

			tmp = p.getProperty(PROPERTY_MIN_LENGTH);
			if(tmp!=null){
				tmp=tmp.trim();
				try{
					this.minLenght = Integer.parseInt(tmp);
				}catch(Exception e){
					throw new Exception("Property '"+PROPERTY_MIN_LENGTH+"' with wrong value '"+tmp+"': "+e.getMessage(),e);
				}
			}
			
			tmp = p.getProperty(PROPERTY_MAX_LENGTH);
			if(tmp!=null){
				tmp=tmp.trim();
				try{
					this.maxLenght = Integer.parseInt(tmp);
				}catch(Exception e){
					throw new Exception("Property '"+PROPERTY_MAX_LENGTH+"' with wrong value '"+tmp+"': "+e.getMessage(),e);
				}
			}
			
			tmp = p.getProperty(PROPERTY_INCLUDE_LOWER_CASE_LETTER);
			if(tmp!=null){
				tmp=tmp.trim();
				try{
					this.includeLowerCaseLetter = Boolean.parseBoolean(tmp);
				}catch(Exception e){
					throw new Exception("Property '"+PROPERTY_INCLUDE_LOWER_CASE_LETTER+"' with wrong value '"+tmp+"': "+e.getMessage(),e);
				}
			}
			
			tmp = p.getProperty(PROPERTY_INCLUDE_UPPER_CASE_LETTER);
			if(tmp!=null){
				tmp=tmp.trim();
				try{
					this.includeUpperCaseLetter = Boolean.parseBoolean(tmp);
				}catch(Exception e){
					throw new Exception("Property '"+PROPERTY_INCLUDE_UPPER_CASE_LETTER+"' with wrong value '"+tmp+"': "+e.getMessage(),e);
				}
			}
			
			tmp = p.getProperty(PROPERTY_INCLUDE_NUMBER);
			if(tmp!=null){
				tmp=tmp.trim();
				try{
					this.includeNumber = Boolean.parseBoolean(tmp);
				}catch(Exception e){
					throw new Exception("Property '"+PROPERTY_INCLUDE_NUMBER+"' with wrong value '"+tmp+"': "+e.getMessage(),e);
				}
			}
			
			tmp = p.getProperty(PROPERTY_INCLUDE_NOT_ALPHANUMERIC_SYMBOL);
			if(tmp!=null){
				tmp=tmp.trim();
				try{
					this.includeNotAlphanumericSymbol = Boolean.parseBoolean(tmp);
				}catch(Exception e){
					throw new Exception("Property '"+PROPERTY_INCLUDE_NOT_ALPHANUMERIC_SYMBOL+"' with wrong value '"+tmp+"': "+e.getMessage(),e);
				}
			}
			
			tmp = p.getProperty(PROPERTY_ALL_DISTINCT_CHARACTERS);
			if(tmp!=null){
				tmp=tmp.trim();
				try{
					this.allDistinctCharacters = Boolean.parseBoolean(tmp);
				}catch(Exception e){
					throw new Exception("Property '"+PROPERTY_ALL_DISTINCT_CHARACTERS+"' with wrong value '"+tmp+"': "+e.getMessage(),e);
				}
			}
			
			tmp = p.getProperty(PROPERTY_EXPIRE);
			if(tmp!=null){
				tmp=tmp.trim();
				try{
					this.expireDays = Integer.parseInt(tmp);
					this.checkPasswordExpire = this.expireDays>0;
				}catch(Exception e){
					throw new Exception("Property '"+PROPERTY_EXPIRE+"' with wrong value '"+tmp+"': "+e.getMessage(),e);
				}
			}

			tmp = p.getProperty(PROPERTY_HISTORY);
			if(tmp!=null){
				tmp=tmp.trim();
				try{
					this.history = Boolean.parseBoolean(tmp);
				}catch(Exception e){
					throw new Exception("Property '"+PROPERTY_HISTORY+"' with wrong value '"+tmp+"': "+e.getMessage(),e);
				}
			}
			
		}catch(Exception e){
			throw new UtilsException(e.getMessage(),e);
		}
	}
	
	public List<String> getRegulaExpressions() {
		return this.regulaExpressions;
	}
	public void setRegulaExpressions(List<String> regulaExpressions) {
		this.regulaExpressions = regulaExpressions;
	}
	public boolean isNotContainsLogin() {
		return this.notContainsLogin;
	}
	public void setNotContainsLogin(boolean notContainsLogin) {
		this.notContainsLogin = notContainsLogin;
	}
	public List<String> getRestrictedWords() {
		return this.restrictedWords;
	}
	public void setRestrictedWords(List<String> restrictedWords) {
		this.restrictedWords = restrictedWords;
	}
	public int getMinLenght() {
		return this.minLenght;
	}
	public void setMinLenght(int minLenght) {
		this.minLenght = minLenght;
	}
	public int getMaxLenght() {
		return this.maxLenght;
	}
	public void setMaxLenght(int maxLenght) {
		this.maxLenght = maxLenght;
	}
	public boolean isIncludeLowerCaseLetter() {
		return this.includeLowerCaseLetter;
	}
	public void setIncludeLowerCaseLetter(boolean includeLowerCaseLetter) {
		this.includeLowerCaseLetter = includeLowerCaseLetter;
	}
	public boolean isIncludeUpperCaseLetter() {
		return this.includeUpperCaseLetter;
	}
	public void setIncludeUpperCaseLetter(boolean includeUpperCaseLetter) {
		this.includeUpperCaseLetter = includeUpperCaseLetter;
	}
	public boolean isIncludeNumber() {
		return this.includeNumber;
	}
	public void setIncludeNumber(boolean includeNumber) {
		this.includeNumber = includeNumber;
	}
	public boolean isIncludeNotAlphanumericSymbol() {
		return this.includeNotAlphanumericSymbol;
	}
	public void setIncludeNotAlphanumericSymbol(boolean includeNotAlphanumericSymbol) {
		this.includeNotAlphanumericSymbol = includeNotAlphanumericSymbol;
	}
	public boolean isAllDistinctCharacters() {
		return this.allDistinctCharacters;
	}
	public void setAllDistinctCharacters(boolean allDistinctCharacters) {
		this.allDistinctCharacters = allDistinctCharacters;
	}
	public int getExpireDays() {
		return this.expireDays;
	}
	public void setExpireDays(int expireDays) {
		this.expireDays = expireDays;
		this.checkPasswordExpire = this.expireDays>0;
	}
	public boolean isCheckPasswordExpire() {
		return this.checkPasswordExpire;
	}
	public boolean isHistory() {
		return this.history;
	}
	public void setHistory(boolean history) {
		this.history = history;
	}
	
	public boolean validate(String login, String password){
		StringBuilder bf = new StringBuilder();
		return this.validate(login,password,bf);
	}
	public boolean validate(String login, String password,StringBuilder bfMotivazioneErrore){
		if(password==null) {
			bfMotivazioneErrore.append("Password non fornita");
			return false;
		}
		password = password.trim();
		if(this.regulaExpressions.size()>0){
			for (String regExp : this.regulaExpressions) {
				try{
					if (RegularExpressionEngine.isMatch(password, regExp) == false){
						bfMotivazioneErrore.append("La password non rispetta l'espressione regolare: "+regExp);
						return false;
					}
				}catch(Exception e){
					bfMotivazioneErrore.append("Rilevato un errore durante la verifica della password con l'espressione regolare '"+regExp+"': "+e.getMessage());
					return false;
				}
			}
		}
		if(this.notContainsLogin){
			if(password.contains(login)){
				bfMotivazioneErrore.append("La password contiene il nome di login");
				return false;
			}
		}
		if(this.restrictedWords.size()>0){
			for (String word : this.restrictedWords) {
				if(password.toLowerCase().equals(word)){
					bfMotivazioneErrore.append("La password corrisponde ad una delle parole riservate: "+word);
					return false;
				}
			}
		}
		if(this.minLenght>0){
			if(password.length()<this.minLenght){
				bfMotivazioneErrore.append("La password deve essere composta almeno da ").append(this.minLenght).
					append(" caratteri mentre quella fornita ha una lunghezza di ").append(password.length()).append(" caratteri");
				return false;
			}
		}
		if(this.maxLenght>0){
			if(password.length()>this.maxLenght){
				bfMotivazioneErrore.append("La password non deve essere composta da più di ").append(this.minLenght).
					append(" caratteri mentre quella fornita ha una lunghezza di ").append(password.length()).append(" caratteri");
				return false;
			}
		}
		if(this.includeLowerCaseLetter){
			boolean found = false;
			for (int i = 0; i < password.length(); i++) {
				String c = password.charAt(i)+"";
				if(StringUtils.isAllLowerCase(c)){
					found = true;
					break;
				}
			}
			if(!found){
				bfMotivazioneErrore.append("La password deve contenere almeno una lettera minuscola (a - z)");
				return false;
			}
		}
		if(this.includeUpperCaseLetter){
			boolean found = false;
			for (int i = 0; i < password.length(); i++) {
				String c = password.charAt(i)+"";
				if(StringUtils.isAllUpperCase(c)){
					found = true;
					break;
				}
			}
			if(!found){
				bfMotivazioneErrore.append("La password deve contenere almeno una lettera maiuscola (A - Z)");
				return false;
			}
		}
		if(this.includeNumber){
			boolean found = false;
			for (int i = 0; i < password.length(); i++) {
				String c = password.charAt(i)+"";
				if(StringUtils.isNumeric(c)){
					found = true;
					break;
				}
			}
			if(!found){
				bfMotivazioneErrore.append("La password deve contenere almeno un numero (0 - 9)");
				return false;
			}
		}
		if(this.includeNotAlphanumericSymbol){
			boolean found = false;
			for (int i = 0; i < password.length(); i++) {
				String c = password.charAt(i)+"";
				if(!StringUtils.isNumeric(c) && !StringUtils.isAlpha(c)){
					found = true;
					break;
				}
			}
			if(!found){
				bfMotivazioneErrore.append("La password deve contenere almeno un carattere non alfanumerico (ad esempio, !, $, #, %, @)");
				return false;
			}
		}
		if(this.allDistinctCharacters){
			for (int i = 0; i < password.length(); i++) {
				String c = password.charAt(i)+"";
				int count = 0;
				for (int j = 0; j < password.length(); j++) {
					String check = password.charAt(j)+"";
					if(check.equals(c)){
						count++;
					}
				}
				if(count>1){
					bfMotivazioneErrore.append("Tutti i caratteri utilizzati devono essere differenti mentre nella password fornita il carattere '"+c+"' appare "+count+" volte");
					return false;
				}
			}
		}
		return true;
	}
	
	public boolean isPasswordExpire(Date lastUpdatePassword){
		StringBuilder bf = new StringBuilder();
		return this.isPasswordExpire(lastUpdatePassword,bf);
	}
	public boolean isPasswordExpire(Date lastUpdatePassword, StringBuilder bfMotivazioneErrore) {
		if(this.checkPasswordExpire) {
			Date now = DateManager.getDate();
			long expireMs = ((long) this.expireDays * 24 * 60 * 60 * 1000);
			Date expireDate = new Date(lastUpdatePassword.getTime() + expireMs );
			if(expireDate.before(now)) {
				bfMotivazioneErrore.append("Password impostata da più di "+this.expireDays+" giorni");
				return true;
			}
		}
		return false;
	}
	
	public boolean existsRestriction(){
		String s = this.help("", "", false, false);
		return s != null && !"".equals(s);
	}
	
	public String help(){
		return this.help("\n", "- ", true, false);
	}
	public String help(String separator){
		return this.help(separator, "- ", true, false);
	}
	public boolean existsRestrictionUpdate(){
		String s = this.help("", "", false, true);
		return s != null && !"".equals(s);
	}
	
	public String helpUpdate(){
		return this.help("\n", "- ", true, true);
	}
	public String helpUpdate(String separator){
		return this.help(separator, "- ", true, true);
	}
	public String help(String separator, String elenco, boolean premessa, boolean update){
		StringBuilder bf = new StringBuilder();
		if(premessa){
			bf.append("La password deve rispettare i seguenti vincoli: ");
		}
		if(this.regulaExpressions.size()>0){
			for (String regExp : this.regulaExpressions) {
				bf.append(separator);
				bf.append(elenco).append("deve soddisfare l'espressione regolare: "+regExp);
			}
		}
		if(this.notContainsLogin){
			bf.append(separator);
			bf.append(elenco).append("non deve contenere il nome di login dell'utente");
		}
		if(this.restrictedWords.size()>0){
			bf.append(separator);
			bf.append(elenco).append("non deve corrispondere ad una delle seguenti parole riservate: "+this.restrictedWords);
		}
		if(this.minLenght>0){
			bf.append(separator);
			bf.append(elenco).append("deve essere composta almeno da ").append(this.minLenght).append(" caratteri");
		}
		if(this.maxLenght>0){
			bf.append(separator);
			bf.append(elenco).append("non deve essere composta da più di ").append(this.maxLenght).append(" caratteri");
		}
		if(this.includeLowerCaseLetter){
			bf.append(separator);
			bf.append(elenco).append("deve contenere almeno una lettera minuscola (a - z)");
		}
		if(this.includeUpperCaseLetter){
			bf.append(separator);
			bf.append(elenco).append("deve contenere almeno una lettera maiuscola (A - Z)");
		}
		if(this.includeNumber){
			bf.append(separator);
			bf.append(elenco).append("deve contenere almeno un numero (0 - 9)");
		}
		if(this.includeNotAlphanumericSymbol){
			bf.append(separator);
			bf.append(elenco).append("deve contenere almeno un carattere non alfanumerico (ad esempio, !, $, #, %, @)");
		}
		if(this.allDistinctCharacters){
			bf.append(separator);
			bf.append(elenco).append("tutti i caratteri utilizzati devono essere differenti");
		}
		if(this.history){
			if(update) {
				bf.append(separator);
				bf.append(elenco).append("non deve corrispondere ad una precedente password");
			}
		}
		return bf.toString();
	}
}