BYOKManager.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.certificate.byok;

import java.io.File;
import java.io.FileInputStream;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Properties;

import org.apache.commons.lang.StringUtils;
import org.openspcoop2.utils.SortedMap;
import org.openspcoop2.utils.Utilities;
import org.openspcoop2.utils.UtilsException;
import org.openspcoop2.utils.properties.PropertiesReader;
import org.slf4j.Logger;

/**
 * BYOKManager
 *
 * @author Poli Andrea (apoli@link.it)
 * @author $Author$
 * @version $Rev$, $Date$
 */
public class BYOKManager {

	private static BYOKManager staticInstance;
	public static synchronized void init(File f, boolean throwNotExists, Logger log) throws UtilsException {
		if(staticInstance==null) {
			staticInstance = new BYOKManager(f, throwNotExists, log);
		}
	}
	public static BYOKManager getInstance() {
		// spotbugs warning 'SING_SINGLETON_GETTER_NOT_SYNCHRONIZED': l'istanza viene creata allo startup
		if (staticInstance == null) {
	        synchronized (BYOKManager.class) {
	            if (staticInstance == null) {
	                return null;
	            }
	        }
	    }
		return staticInstance;
	}
	public static String getSecurityEngineGovWayPolicy() {
		if(staticInstance!=null) {
			return staticInstance.getSecurityEngineGovWay();
		}
		return null;
	}
	public static String getSecurityRemoteEngineGovWayPolicy() {
		if(staticInstance!=null) {
			return staticInstance.getSecurityRemoteEngineGovWay();
		}
		return null;
	}
	public static boolean isEnabledBYOK() {
		String securityManagerPolicy = BYOKManager.getSecurityEngineGovWayPolicy();
		return securityManagerPolicy!=null && StringUtils.isNotEmpty(securityManagerPolicy);
	}
	
	/*
	 * Consente di inizializzare una serie di keystore hardware
	 * 
	 * La configurazione di ogni keystore deve essere definita nel file ksm.properties fornito come argomento dove la sintassi utilizzabile è la seguente
	 * 
	 * ksm.<idKSM>.type: [required] identificativo univoco del ksm utilizzato nelle configurazioni di govway
	 * ksm.<idKSM>.label: [required] etichetta associata al ksm e visualizzata nelle maschere di configurazione
	 * ksm.<idKSM>.mode: [required] indica il tipo di operazione wrap e unwrap
	 * ksm.<idKSM>.input.<idParam>.name: [optional] se fornito definisce un parametro richiesto alla configurazione della credenziale a cui il ksm viene associato 
	 * ksm.<idKSM>.input.<idParam>.label: [optional] definisce l'etichetta associata al parametro di configurazione
	 * 
	 * Le restanti proprietà definiscono come accedere al ksm possono essere valorizzate utilizzando anche le seguenti variabili:
	 * ${ksm-key}: byte[] dell'archivio indicato
	 * ${ksm-base64-key}: byte[] dell'archivio indicato codificato in base64
	 * ${ksm-hex-key}: byte[] dell'archivio indicato codificato in base64
	 * ${ksm:<nomeparametro>}: parametro indicato nella configurazione del ksm
	 * 
	 * L'operazione di wrap/unwrap può essere realizzata invocando un ksm remoto (via http) o cifrando/decifrando tramite keystore locali.
	 * 
	 * ksm.<idKSM>.encryptionMode: [optional; default:remote] indica il tipo di encryption via ksm remoto (remote) o locale (local)
	 * 
	 * Configurazioni per ksm remoto:
	 * 
	 * ksm.<idKSM>.http.endpoint: [required] definisce l'endpoint del ksm
	 * ksm.<idKSM>.http.method: [required] definisce il metodo HTTP utilzzato per connettersi al ksm
	 * ksm.<idKSM>.http.header.<nome>: definisce un header HTTP che possiede il nome indicato nella proprietà stessa
	 * ksm.<idKSM>.http.payload.inline [optional] definisce il payload da utilizzare nella richiesta http
	 * ksm.<idKSM>.http.payload.path [optional] alternativa alla precedente proprietà defnisce il path ad un file contenente il payload da utilizzare nella richiesta http
	 * ksm.<idKSM>.http.username [optional] definisce la credenziale http-basic (username)
	 * ksm.<idKSM>.http.password [optional] definisce la credenziale http-basic (password)
	 * ksm.<idKSM>.http.connectionTimeout [optional; int] tempo massimo in millisecondi di attesa per stabilire una connessione con il server ksm
	 * ksm.<idKSM>.http.readTimeout [optional; int] tempo massimo in millisecondi di attesa per la ricezione di una risposta dal server
	 * 
	 * Le seguenti proprietà opzionali consentono invece di utilizzare e configurare un connettore di tipo https
	 * ksm.<idKSM>.https [optional; boolean] indica se utilizzare o meno un connettore di tipo https 
	 * ksm.<idKSM>.https.hostnameVerifier  [optional; boolean] indica se deve essere verificato l'hostname rispetto al certificato server
	 * 
	 * ksm.<idKSM>.https.serverAuth  [optional; boolean] indica se deve essere effettuata l'autenticazione del certificato server
	 * ksm.<idKSM>.https.serverAuth.trustStore.path: truststore per effettuare l'autenticazione
	 * ksm.<idKSM>.https.serverAuth.trustStore.type: tipo di truststore
	 * ksm.<idKSM>.https.serverAuth.trustStore.password password del truststore
	 * ksm.<idKSM>.https.serverAuth.trustStore.crls: crl
	 * ksm.<idKSM>.https.serverAuth.trustStore.ocspPolicy: OCSP Policy
	 * 
	 * ksm.<idKSM>.https.clientAuth  [optional; boolean] indica se deve essere inviato un certificato client
	 * ksm.<idKSM>.https.clientAuth.keyStore.path: keystore per effettuare l'autenticazione client
	 * ksm.<idKSM>.https.clientAuth.keyStore.type: tipo di keystore
	 * ksm.<idKSM>.https.clientAuth.keyStore.password password del keystore
	 * ksm.<idKSM>.https.clientAuth.key.alias: identifica la chiave privata
	 * ksm.<idKSM>.https.clientAuth.key.password: password della chiave privata
     *
     * ksm.<idKSM>.http.response.base64Encoded [optional; boolean] indicazione se la risposta è codificata in base64
     * ksm.<idKSM>.http.response.hexEncoded [optional; boolean] indicazione se la risposta è codificata tramite una rappresentazione esadecimale
     * ksm.<idKSM>.http.response.jsonPath [optional] se la risposta è un json (eventualmente dopo la decodificata base64/hex) consente di indicare un jsonPath per estrarre l'informazione da un singolo elemento
     *
     * Configurazioni per ksm locale:
     * 
     * ksm.<idKSM>.TERMINARE
	 * 
	 **/
	

	private HashMap<String, BYOKConfig> ksmKeystoreMapIDtoConfig = new HashMap<>();
	
	private HashMap<String, String> ksmKeystoreMapLabelToID = new HashMap<>();
	private HashMap<String, String> ksmKeystoreMapTypeToID = new HashMap<>();
	
	private List<String> unwrapTypes = new ArrayList<>();
	private List<String> unwrapLabels = new ArrayList<>();
	private List<String> wrapTypes = new ArrayList<>();
	private List<String> wrapLabels = new ArrayList<>();

	private static final String UNKNOWN = "unknown";
	
	private HashMap<String, BYOKSecurityConfig> securityMapIDtoConfig = new HashMap<>();
	
	private String securityEngineGovWay = null;
	private String securityRemoteEngineGovWay = null;
	
	private BYOKManager(File f, boolean throwNotExists, Logger log) throws UtilsException {
		String prefixFile = "File '"+f.getAbsolutePath()+"'";
		if(!f.exists()) {
			if(throwNotExists) {
				throw new UtilsException(prefixFile+" not exists");
			}
		}
		else {
			if(!f.canRead()) {
				throw new UtilsException(prefixFile+" cannot read");
			}
			Properties p = new Properties();
			try {
				try(FileInputStream fin = new FileInputStream(f)){
					p.load(fin);
				}
			}catch(Exception t) {
				throw new UtilsException(prefixFile+"; initialize error: "+t.getMessage(),t);
			}
			init(p, log);
		}
	}
	/**private KSMManager(Properties p, Logger log, boolean accessKeystore) throws UtilsException {
		init(p, log, accessKeystore);
	}*/
	private void init(Properties p, Logger log) throws UtilsException {
		
		List<String> idKeystore = new ArrayList<>();
		List<String> securityKeystore = new ArrayList<>();
		
		if(p!=null && !p.isEmpty()) {
			init(p, idKeystore, securityKeystore);
		}
		
		if(!idKeystore.isEmpty()) {
			for (String idK : idKeystore) {
				init(p, log, idK);		
			}
		}
		else {
			log.warn("La configurazione fornita per KSM non contiene alcun keystore");
		}
		
		if(!securityKeystore.isEmpty()) {
			for (String idK : securityKeystore) {
				initSecurity(p, log, idK);		
			}
		}
		else {
			log.warn("La configurazione fornita per KSM non contiene alcun security manager");
		}
		
		initSecurityGovWay(p);
	}
	private void init(Properties p, List<String> idKeystore, List<String> securityKeystore) {
		Enumeration<?> enKeys = p.keys();
		while (enKeys.hasMoreElements()) {
			Object object = enKeys.nextElement();
			if(object instanceof String) {
				String key = (String) object;
				init(key, idKeystore, securityKeystore);	
			}
		}
	}
	private void init(String key, List<String> idKeystore, List<String> securityKeystore) {
		boolean isIdKeystore = initEngine(key, idKeystore, BYOKCostanti.PROPERTY_PREFIX);
		if(!isIdKeystore) {
			initEngine(key, securityKeystore, BYOKCostanti.SECURITY_PROPERTY_PREFIX);
		}
	}
	private boolean initEngine(String key, List<String> list, String prefix) {
		if(key.startsWith(prefix) && key.length()>(prefix.length())) {
			String tmp = key.substring(prefix.length());
			if(tmp!=null && tmp.contains(".")) {
				int indeoOf = tmp.indexOf(".");
				if(indeoOf>0) {
					String idK = tmp.substring(0,indeoOf);
					if(!list.contains(idK)) {
						list.add(idK);
					}
				}
			}
			return true;
		}
		return false;
	}
	
	private void init(Properties p, Logger log, String idK) throws UtilsException {
		String prefix = BYOKCostanti.PROPERTY_PREFIX + idK + ".";
		Properties pKeystore = Utilities.readProperties(prefix, p);
		BYOKConfig ksmKeystore = new BYOKConfig(idK, pKeystore, log);
		
		// check label
		boolean alreadyExists = false;
		for (String l : this.ksmKeystoreMapLabelToID.keySet()) {
			if(ksmKeystore.getLabel().equalsIgnoreCase(l)) {
				alreadyExists = true;
				break;
			}
		}
		if(alreadyExists) {
			throw new UtilsException("Same label found for ksm '"+this.ksmKeystoreMapLabelToID.get(ksmKeystore.getLabel())+"' e '"+idK+"'");
		}
		this.ksmKeystoreMapLabelToID.put(ksmKeystore.getLabel(), idK);
		
		// check type
		alreadyExists = false;
		for (String type : this.ksmKeystoreMapTypeToID.keySet()) {
			if(ksmKeystore.getType().equalsIgnoreCase(type)) {
				alreadyExists = true;
				break;
			}
		}
		if(alreadyExists) {
			throw new UtilsException("Same type found for ksm '"+this.ksmKeystoreMapTypeToID.get(ksmKeystore.getType())+"' e '"+idK+"'");
		}
		this.ksmKeystoreMapTypeToID.put(ksmKeystore.getType(), idK);
		
		// registro nelle liste
		if(BYOKMode.UNWRAP.equals(ksmKeystore.getMode())) {
			this.unwrapTypes.add(ksmKeystore.getType());
			this.unwrapLabels.add(ksmKeystore.getLabel());
		}
		else {
			this.wrapTypes.add(ksmKeystore.getType());
			this.wrapLabels.add(ksmKeystore.getLabel());
		}
		
		this.ksmKeystoreMapIDtoConfig.put(idK, ksmKeystore);
		String d = "KSM "+idK+" registrato (type:"+ksmKeystore.getType()+") label:"+ksmKeystore.getLabel()+"";
		log.info(d);	
	}
	
	private void initSecurity(Properties p, Logger log, String idK) throws UtilsException {
		String prefix = BYOKCostanti.SECURITY_PROPERTY_PREFIX + idK + ".";
		Properties pKeystore = Utilities.readProperties(prefix, p);
		BYOKSecurityConfig securityConfig = new BYOKSecurityConfig(idK, pKeystore, log);
				
		this.securityMapIDtoConfig.put(idK, securityConfig);
		String d = "Security manager "+idK+" registrato";
		log.info(d);	
	}
	
	private void initSecurityGovWay(Properties p) throws UtilsException {
		
		PropertiesReader pReader = new PropertiesReader(p, true);
		
		this.securityEngineGovWay = pReader.getValue_convertEnvProperties(BYOKCostanti.PROPERTY_GOVWAY_SECURITY);
		if(this.securityEngineGovWay!=null && StringUtils.isEmpty(this.securityEngineGovWay)) {
			this.securityEngineGovWay = null;
		}
		
		this.securityRemoteEngineGovWay = pReader.getValue_convertEnvProperties(BYOKCostanti.PROPERTY_GOVWAY_SECURITY_RUNTIME);
		if(this.securityRemoteEngineGovWay!=null && StringUtils.isEmpty(this.securityRemoteEngineGovWay)) {
			this.securityRemoteEngineGovWay = null;
		}
	}
	
	public BYOKConfig getKSMConfigByType(String type) throws UtilsException {
		if(!this.ksmKeystoreMapTypeToID.containsKey(type)) {
			throw new UtilsException("KSM type '"+type+"' "+UNKNOWN);
		}
		String idK = this.ksmKeystoreMapTypeToID.get(type);
		if(!this.ksmKeystoreMapIDtoConfig.containsKey(idK)) {
			throw new UtilsException("KSM config for type '"+type+"' unknown ? (id:"+idK+")");
		}
		return this.ksmKeystoreMapIDtoConfig.get(idK);
	}
	
	public BYOKConfig getKSMConfigByLabel(String label) throws UtilsException {
		if(!this.ksmKeystoreMapLabelToID.containsKey(label)) {
			throw new UtilsException("KSM label '"+label+"' "+UNKNOWN);
		}
		String idK = this.ksmKeystoreMapLabelToID.get(label);
		if(!this.ksmKeystoreMapIDtoConfig.containsKey(idK)) {
			throw new UtilsException("KSM config for label '"+label+"' unknown ? (id:"+idK+")");
		}
		return this.ksmKeystoreMapIDtoConfig.get(idK);
	}
	
	public List<String> getKeystoreTypes() {
		List<String> l = new ArrayList<>();
		if(!this.ksmKeystoreMapLabelToID.isEmpty()) {
			for (String type : this.ksmKeystoreMapLabelToID.keySet()) {
				l.add(type);
			}
		}
		return l;
	}
	
	public SortedMap<String> getKeystoreWrapConfigTypesLabels() throws UtilsException {
		return getKeystoreConfigTypesLabels(true);
	}
	public SortedMap<String> getKeystoreUnwrapConfigTypesLabels() throws UtilsException {
		return getKeystoreConfigTypesLabels(false);
	}
	private SortedMap<String> getKeystoreConfigTypesLabels(boolean wrap) throws UtilsException {
		
		SortedMap<String> sMap = new SortedMap<>();
		
		List<String> types = wrap ? this.wrapTypes : this.unwrapTypes;
		List<String> labels = wrap ? this.wrapLabels : this.unwrapLabels;
		
		// SortedMap by label
		if(types!=null && !types.isEmpty()) {
			List<String> labelsDaOrdinare = new ArrayList<>(); 
			Map<String, String> m = new HashMap<>();
			for (int i = 0; i < types.size(); i++) {
				String type = types.get(i);
				String label = labels.get(i);
				m.put(label, type);
				labelsDaOrdinare.add(label);
			}
			
			Collections.sort(labelsDaOrdinare);
			for (String l : labelsDaOrdinare) {
				sMap.add(m.get(l), l);	
			}
		}
		
		return sMap;
	}
	
	public boolean isKSMUsedInSecurityWrapConfig(String id, StringBuilder securityId) {
		return isKSMUsedInSecurityConfig(true, id, securityId);
	}
	public boolean isKSMUsedInSecurityUnwrapConfig(String id, StringBuilder securityId) {
		return isKSMUsedInSecurityConfig(false, id, securityId	);
	}
	private boolean isKSMUsedInSecurityConfig(boolean wrap, String id, StringBuilder securityId) {
		if(!this.securityMapIDtoConfig.isEmpty()) {
			for (Map.Entry<String,BYOKSecurityConfig> entry : this.securityMapIDtoConfig.entrySet()) {
				String confKsmId = wrap ? entry.getValue().getWrapId() : entry.getValue().getUnwrapId();
				if(id.equals(confKsmId)){
					if(securityId!=null) {
						securityId.append(entry.getKey());
					}
					return true;
				}
			}
		}
		return false;
	}
	
	public boolean existsKSMConfigByType(String type) {
		if(type==null) {
			return false;
		}
		for (String i : this.ksmKeystoreMapTypeToID.keySet()) {
			if(type.equalsIgnoreCase(i)) {
				return true;
			}
		}
		return false;
	}
	
	public boolean existsKSMConfigByLabel(String label) {
		if(label==null) {
			return false;
		}
		for (String i : this.ksmKeystoreMapLabelToID.keySet()) {
			if(label.equalsIgnoreCase(i)) {
				return true;
			}
		}
		return false;
	}
	
	public List<String> getUnwrapTypes() {
		return this.unwrapTypes;
	}
	public List<String> getUnwrapLabels() {
		return this.unwrapLabels;
	}
	public List<String> getWrapTypes() {
		return this.wrapTypes;
	}
	public List<String> getWrapLabels() {
		return this.wrapLabels;
	}
	
	public BYOKSecurityConfig getKSMSecurityConfig(String type) throws UtilsException {
		if(!this.securityMapIDtoConfig.containsKey(type)) {
			throw new UtilsException("KSM security config type '"+type+"' "+UNKNOWN);
		}
		BYOKSecurityConfig c = this.securityMapIDtoConfig.get(type);
		if(c==null) {
			throw new UtilsException("KSM security config type '"+type+"' "+UNKNOWN);
		}
		return c;
	}
	public List<String> getSecurityEngineTypes() {
		List<String> l = new ArrayList<>();
		if(!this.securityMapIDtoConfig.isEmpty()) {
			for (String type : this.securityMapIDtoConfig.keySet()) {
				l.add(type);
			}
		}
		return l;
	}
	
	public boolean existsSecurityEngineByType(String type) {
		if(type==null) {
			return false;
		}
		for (String i : this.securityMapIDtoConfig.keySet()) {
			if(type.equalsIgnoreCase(i)) {
				return true;
			}
		}
		return false;
	}
	
	
	public boolean isBYOKRemoteGovWayNodeUnwrapConfig() throws UtilsException {
		return isBYOKRemoteGovWayNodeConfig(BYOKManager.getSecurityEngineGovWayPolicy(), false, true);
	}
	public boolean isBYOKRemoteGovWayNodeWrapConfig() throws UtilsException {
		return isBYOKRemoteGovWayNodeConfig(BYOKManager.getSecurityEngineGovWayPolicy(), true, false);
		
	}
	
	public boolean isBYOKRemoteGovWayNodeUnwrapConfig(String securityManagerPolicy) throws UtilsException {
		return isBYOKRemoteGovWayNodeConfig(securityManagerPolicy, false, true);
	}
	public boolean isBYOKRemoteGovWayNodeWrapConfig(String securityManagerPolicy) throws UtilsException {
		return isBYOKRemoteGovWayNodeConfig(securityManagerPolicy, true, false);
		
	}
	public boolean isBYOKRemoteGovWayNodeConfig(String securityManagerPolicy) throws UtilsException {
		return isBYOKRemoteGovWayNodeConfig(securityManagerPolicy, true, true);
	}
	public boolean isBYOKRemoteGovWayNodeConfig(String securityManagerPolicy, boolean wrap, boolean unwrap) throws UtilsException {
		
		if(securityManagerPolicy==null || StringUtils.isEmpty(securityManagerPolicy)) {
			return false;
		}
		
		BYOKSecurityConfig secConfig = this.getKSMSecurityConfig(securityManagerPolicy);
		
		boolean govwayRuntime = false;
		if(secConfig.getInputParameters()!=null && !secConfig.getInputParameters().isEmpty()) {
			for (BYOKSecurityConfigParameter sec : secConfig.getInputParameters()) {
				if(sec.getValue().contains(("${"+BYOKCostanti.GOVWAY_RUNTIME_CONTEXT+":"))) {
					govwayRuntime = true;
					break;
				}
			}
		}
		if(!govwayRuntime) {
			return false;
		}
		
		return isBYOKRemoteGovWayNodeConfig(secConfig, wrap, unwrap);		
	}
	private boolean isBYOKRemoteGovWayNodeConfig(BYOKSecurityConfig secConfig, boolean wrap, boolean unwrap) throws UtilsException {
		// ne basta uno
		
		if(wrap) {
			BYOKConfig c = this.getKSMConfigByType(secConfig.getWrapId());
			if(BYOKEncryptionMode.REMOTE.equals(c.getEncryptionMode())) {
				return true;
			}
		}
		
		if(unwrap) {
			BYOKConfig c = this.getKSMConfigByType(secConfig.getUnwrapId());
			if(BYOKEncryptionMode.REMOTE.equals(c.getEncryptionMode())) {
				return true;
			}
		}
		
		return false;
	}
	
	private String getSecurityEngineGovWay() {
		if(this.securityEngineGovWay==null || StringUtils.isEmpty(this.securityEngineGovWay)) {
			return null;
		}
		return this.securityEngineGovWay;
	}
	private String getSecurityRemoteEngineGovWay() {
		if(this.securityRemoteEngineGovWay==null || StringUtils.isEmpty(this.securityRemoteEngineGovWay)) {
			return null;
		}
		return this.securityRemoteEngineGovWay;
	}
	public String getSecurityEngineGovWayDescription() {
		if(this.securityEngineGovWay==null || StringUtils.isEmpty(this.securityEngineGovWay)) {
			return "unactive";
		}
		else {
			StringBuilder sb = new StringBuilder(this.securityEngineGovWay);
			if(this.securityRemoteEngineGovWay!=null && StringUtils.isNotEmpty(this.securityRemoteEngineGovWay) && 
					!this.securityEngineGovWay.equals(this.securityRemoteEngineGovWay)) {
				sb.append(" (remote:").append(this.securityRemoteEngineGovWay).append(")");
			}
			return sb.toString();
		}
	}
}