/*******************************************************************************
* Copyright (C) 2010 Marco Sandrini
*
* 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.casbah.provider;
import java.io.BufferedReader;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.StringReader;
import java.security.GeneralSecurityException;
import java.security.KeyFactory;
import java.security.MessageDigest;
import java.security.SecureRandom;
import java.security.interfaces.RSAPrivateCrtKey;
import java.util.Properties;
import javax.crypto.Cipher;
import javax.crypto.SecretKey;
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.DESedeKeySpec;
import javax.crypto.spec.IvParameterSpec;
import org.apache.commons.codec.binary.Base64;
import org.apache.commons.codec.binary.Hex;
import org.apache.commons.io.IOUtils;
import org.casbah.common.PemEncoder;
public class SSLeayEncoder {
private static final String SUPPORTED_PROC_TYPE = "4,ENCRYPTED";
private static final String SSLEAY_ENC_ALGORITHM = "DES-EDE3-CBC";
private static final String JAVA_ENC_ALGORITHM = "DESede/CBC/PKCS5Padding";
private static final String JAVA_KEY_TYPE = "DESede";
private static final String PROC_TYPE = "Proc-Type";
private static final String DEK_INFO = "DEK-Info";
private static final int SALT_LENGTH = 8;
private static final int BASE64_LINE_LENGTH = 64;
private static final byte[] BASE64_LINE_SEPARATOR = "\n".getBytes();
public static RSAPrivateCrtKey decodeKey(String pemData, String keypass) throws CAProviderException {
BufferedReader reader = null;
try {
String strippedData = PemEncoder.stripArmor(pemData);
String[] portions = strippedData.split("(?m)^\\n");
if ((portions == null) || (portions.length != 3)) {
throw new CAProviderException("Could not extract metainfo from file", null);
}
Properties props = new Properties();
props.load(new StringReader(portions[1]));
String procType = props.getProperty(PROC_TYPE);
if ((procType == null) || (!procType.equals(SUPPORTED_PROC_TYPE))) {
throw new CAProviderException("Missing or invalid Proc-Type declaration", null);
}
String dekInfo = props.getProperty(DEK_INFO);
if (dekInfo == null) {
throw new CAProviderException("Missing DEK-Info declaration", null);
}
String[] infoPortions = dekInfo.split(",");
if ((infoPortions == null) || (infoPortions.length != 2) || (!SSLEAY_ENC_ALGORITHM.equals(infoPortions[0]))) {
throw new CAProviderException("Invalid DEK-Info declaration", null);
}
byte[] decData = decryptKey(portions[2], Hex.decodeHex(infoPortions[1].toCharArray()), keypass);
PKCS1EncodedKeySpec encodedKeySpec = new PKCS1EncodedKeySpec(decData);
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
return (RSAPrivateCrtKey) keyFactory.generatePrivate(encodedKeySpec.toRsaKeySpec());
} catch (CAProviderException cpe) {
throw cpe;
} catch (Exception e) {
throw new CAProviderException("Could not decode SSLeay key", e);
} finally {
IOUtils.closeQuietly(reader);
}
}
public static byte[] decryptKey(String data, byte[] salt, String keypass) throws IOException, GeneralSecurityException {
byte[] encData = Base64.decodeBase64(data);
return performCipherOperation(encData, salt, keypass, Cipher.DECRYPT_MODE);
}
private static byte[] performCipherOperation(byte[] data, byte[] salt, String keypass, int opMode) throws GeneralSecurityException, IOException {
Cipher cipher = Cipher.getInstance(JAVA_ENC_ALGORITHM);
SecretKeyFactory secretKeyFactory = SecretKeyFactory.getInstance(JAVA_KEY_TYPE);
SecretKey secretKey = secretKeyFactory.generateSecret(calculateKeyFromPassKey(keypass.getBytes(), salt));
IvParameterSpec iv = new IvParameterSpec(salt);
cipher.init(opMode, secretKey, iv);
return cipher.doFinal(data);
}
public static String encryptKey(byte[] data, byte[] salt, String keypass) throws GeneralSecurityException, IOException {
byte[] encData = performCipherOperation(data, salt, keypass, Cipher.ENCRYPT_MODE);
return new Base64(BASE64_LINE_LENGTH, BASE64_LINE_SEPARATOR).encodeToString(encData);
}
private static DESedeKeySpec calculateKeyFromPassKey(byte[] keypass, byte[] salt) throws IOException, GeneralSecurityException {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
MessageDigest md = MessageDigest.getInstance("MD5");
byte[] digest = null;
while(baos.size() < 24) {
md.reset();
if (digest != null) {
md.update(digest);
}
md.update(keypass);
digest = md.digest(salt);
baos.write(digest);
}
DESedeKeySpec result = new DESedeKeySpec(baos.toByteArray());
return result;
}
public static String encodeKey(RSAPrivateCrtKey key, String keypass) throws GeneralSecurityException, IOException {
PKCS1EncodedKey pkcs1Key = new PKCS1EncodedKey(key);
byte[] derData = pkcs1Key.getEncoded();
byte[] salt = new byte[SALT_LENGTH];
SecureRandom random = new SecureRandom();
random.nextBytes(salt);
String pemData = encryptKey(derData, salt, keypass);
StringBuffer buffer = new StringBuffer();
buffer.append("-----BEGIN RSA PRIVATE KEY-----\n");
buffer.append(PROC_TYPE + ": " + SUPPORTED_PROC_TYPE +"\n");
buffer.append(DEK_INFO + ": " + SSLEAY_ENC_ALGORITHM + "," + Hex.encodeHexString(salt)+"\n\n");
buffer.append(pemData);
buffer.append("-----END RSA PRIVATE KEY-----\n");
return buffer.toString();
}
}