EncryptOpenSSLPass.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.security;

import java.io.ByteArrayOutputStream;
import java.security.MessageDigest;
import java.util.Arrays;

import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;

import org.apache.commons.lang.StringUtils;
import org.openspcoop2.utils.UtilsException;
import org.openspcoop2.utils.certificate.SymmetricKeyUtils;
import org.openspcoop2.utils.io.Base64Utilities;
import org.openspcoop2.utils.io.HexBinaryUtilities;
import org.openspcoop2.utils.random.RandomUtilities;

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

	 /**
	  * 
	  * Openssl encrypts data using the following steps:
	  * 1. salt = 8-byte cryptographically-strong random number
	  * 2. key = messageDigest("sha256", password+salt)
	  * 3. iv = messageDigest(key+password+salt)[0,16)
	  * 4. cipherTextRaw = encrypt("aes256cbc", key, iv, textPlain)
	  * 5. cipherText = "Salted__"+salt+cipherTextRaw
    */
	public static CipherInfo buildCipherInfo(String password, String digestAlgoParam, OpenSSLEncryptionMode mode) throws UtilsException {
		
		CipherInfo cipherInfo = new CipherInfo();
		
		cipherInfo.setSalt(buildSalt());
		
		cipherInfo.setEncodedKey(buildSecretKey(password, cipherInfo.getSalt(), digestAlgoParam, mode));
		cipherInfo.setKey(new SecretKeySpec(cipherInfo.getEncodedKey(), SymmetricKeyUtils.ALGO_AES));
		
		cipherInfo.setIv(buildIV(password, cipherInfo.getSalt(), cipherInfo.getEncodedKey(), digestAlgoParam));
		cipherInfo.setIvParameterSpec(convertTo(cipherInfo.getIv()));
		
		return cipherInfo;
	}
	static byte[] buildSalt() throws UtilsException {
		try {				 
			// Create salt
			byte[] salt = new byte[8];
			RandomUtilities.getSecureRandom().nextBytes(salt);
			return salt;
		}catch(Exception e) {
			throw new UtilsException(e.getMessage(),e);
		}
	}
	static byte[] buildSecretKey(String password, byte[] salt, String digestAlgoParam, OpenSSLEncryptionMode modeParam) throws UtilsException {
		try {
			// Create key
			byte[] secretKeyClear = password.getBytes();
			ByteArrayOutputStream bout = new ByteArrayOutputStream();
			bout.write(secretKeyClear);
			bout.write(salt);
			bout.flush();
			bout.close();
			byte[]passAndSalt = bout.toByteArray();
			
			String digestAlgo = digestAlgoParam;
			if(digestAlgoParam==null || StringUtils.isEmpty(digestAlgoParam)) {
				digestAlgo = "SHA-256";
			}
			MessageDigest md = MessageDigest.getInstance(digestAlgo);
			byte[] key = md.digest(passAndSalt);
			OpenSSLEncryptionMode mode = modeParam!=null ? modeParam : OpenSSLEncryptionMode.AES_256_CBC;
			switch (mode) {
			case AES_128_CBC:
				key = Arrays.copyOf(key, 16); // AES-128 richiede una chiave di 128 bit (16 byte).
				break;
			case AES_192_CBC:
				key = Arrays.copyOf(key, 24); // AES-192 richiede una chiave di 192 bit (24 byte).
				break;
			case AES_256_CBC:
				key = Arrays.copyOf(key, 32); // AES-256 richiede una chiave di 256 bit (32 byte). 
				break;
			} 
			return key;
		}catch(Exception e) {
			throw new UtilsException(e.getMessage(),e);
		}
	}
	static byte[] buildIV(String password, byte[] salt, byte[]encodedKey, String digestAlgoParam) throws UtilsException {
		try {
			String digestAlgo = digestAlgoParam;
			if(digestAlgoParam==null || StringUtils.isEmpty(digestAlgoParam)) {
				digestAlgo = "SHA-256";
			}
			MessageDigest md = MessageDigest.getInstance(digestAlgo);
			
			// Derive IV
			ByteArrayOutputStream bout = new ByteArrayOutputStream();
			bout.write(encodedKey);
			bout.write(password.getBytes());
			bout.write(salt);
			bout.flush();
			bout.close();
			byte[] keyAndPassAndSalt = bout.toByteArray();
					 
			return Arrays.copyOfRange( md.digest(keyAndPassAndSalt), 0, 16);
		}catch(Exception e) {
			throw new UtilsException(e.getMessage(),e);
		}
	}
	static IvParameterSpec convertTo(byte[] iv) {
		return new IvParameterSpec(iv);
	}

	public static byte[] formatOutput(byte [] salt, byte [] cipherText) throws UtilsException {
		try {
			ByteArrayOutputStream bos = new ByteArrayOutputStream();
			bos.writeBytes("Salted__".getBytes());
			bos.writeBytes(salt);
			bos.writeBytes(cipherText);
			bos.flush();
			bos.close();
			return bos.toByteArray();
		}catch(Exception e) {
			throw new UtilsException(e.getMessage(),e);
		}
	}
	
	private static final String AES_CBC_PKCS5PADDING = "AES/CBC/PKCS5Padding";
	
	
	private CipherInfo cipherInfo;
	private OpenSSLEncryptionMode mode;
	
	public EncryptOpenSSLPass(String password) throws UtilsException {
		this(password, null);
	}
	public EncryptOpenSSLPass(String password, OpenSSLEncryptionMode modeParam) throws UtilsException {
		super(javax.crypto.Cipher.ENCRYPT_MODE);
		this.mode = modeParam!=null ? modeParam : OpenSSLEncryptionMode.AES_256_CBC;
		this.cipherInfo = buildCipherInfo(password, null, this.mode);
		this.key = this.cipherInfo.getKey();
		this.ivParameterSpec = this.cipherInfo.getIvParameterSpec();
	}
	
	
	static String getAlgorithm(OpenSSLEncryptionMode mode) throws UtilsException {
		switch (mode) {
		case AES_128_CBC:
		case AES_192_CBC:
		case AES_256_CBC:
			return AES_CBC_PKCS5PADDING;
		} 
		throw new UtilsException("Unsupported mode");
	}
	
	public byte[] encrypt(String data, String charsetName) throws UtilsException{
		return formatOutput(this.cipherInfo.getSalt(), super.process(data, charsetName, getAlgorithm(this.mode)));
	}
	public byte[] encrypt(byte[] data) throws UtilsException{
		return formatOutput(this.cipherInfo.getSalt(), super.process(data, getAlgorithm(this.mode)));
	}
	
	public byte[] encryptBase64(String data, String charsetName) throws UtilsException{
		return Base64Utilities.encode(this.encrypt(data, charsetName));
	}
	public byte[] encryptBase64(byte[] data) throws UtilsException{
		return Base64Utilities.encode(this.encrypt(data));
	}
	
	public String encryptBase64AsString(String data, String charsetName) throws UtilsException{
		return Base64Utilities.encodeAsString(this.encrypt(data, charsetName));
	}
	public String encryptBase64AsString(byte[] data) throws UtilsException{
		return Base64Utilities.encodeAsString(this.encrypt(data));
	}
	
	public char[] encryptHexBinary(String data, String charsetName) throws UtilsException{
		return HexBinaryUtilities.encode(this.encrypt(data, charsetName));
	}
	public char[] encryptHexBinary(byte[] data) throws UtilsException{
		return HexBinaryUtilities.encode(this.encrypt(data));
	}
	
	public String encryptHexBinaryAsString(String data, String charsetName) throws UtilsException{
		return HexBinaryUtilities.encodeAsString(this.encrypt(data, charsetName));
	}
	public String encryptHexBinaryAsString(byte[] data) throws UtilsException{
		return HexBinaryUtilities.encodeAsString(this.encrypt(data));
	}
	
	@Override
	public void initIV(String algorithm) throws UtilsException{
		// NOP
		// Non deve fare nulla questa chiamata, viene gestita dalla funzione sopra l'IV
	}
}