Home | History | Annotate | Download | only in crypto
      1 /*
      2  * Copyright (c) 2006-2011 Christian Plattner. All rights reserved.
      3  * Please refer to the LICENSE.txt for licensing details.
      4  */
      5 package ch.ethz.ssh2.crypto;
      6 
      7 import java.io.BufferedReader;
      8 import java.io.CharArrayReader;
      9 import java.io.IOException;
     10 import java.math.BigInteger;
     11 
     12 import ch.ethz.ssh2.crypto.cipher.AES;
     13 import ch.ethz.ssh2.crypto.cipher.BlockCipher;
     14 import ch.ethz.ssh2.crypto.cipher.CBCMode;
     15 import ch.ethz.ssh2.crypto.cipher.DES;
     16 import ch.ethz.ssh2.crypto.cipher.DESede;
     17 import ch.ethz.ssh2.crypto.digest.MD5;
     18 import ch.ethz.ssh2.signature.DSAPrivateKey;
     19 import ch.ethz.ssh2.signature.RSAPrivateKey;
     20 import ch.ethz.ssh2.util.StringEncoder;
     21 
     22 /**
     23  * PEM Support.
     24  *
     25  * @author Christian Plattner
     26  * @version $Id: PEMDecoder.java 37 2011-05-28 22:31:46Z dkocher (at) sudo.ch $
     27  */
     28 public class PEMDecoder
     29 {
     30 	private static final int PEM_RSA_PRIVATE_KEY = 1;
     31 	private static final int PEM_DSA_PRIVATE_KEY = 2;
     32 
     33 	private static int hexToInt(char c)
     34 	{
     35 		if ((c >= 'a') && (c <= 'f'))
     36 		{
     37 			return (c - 'a') + 10;
     38 		}
     39 
     40 		if ((c >= 'A') && (c <= 'F'))
     41 		{
     42 			return (c - 'A') + 10;
     43 		}
     44 
     45 		if ((c >= '0') && (c <= '9'))
     46 		{
     47 			return (c - '0');
     48 		}
     49 
     50 		throw new IllegalArgumentException("Need hex char");
     51 	}
     52 
     53 	private static byte[] hexToByteArray(String hex)
     54 	{
     55 		if (hex == null)
     56 			throw new IllegalArgumentException("null argument");
     57 
     58 		if ((hex.length() % 2) != 0)
     59 			throw new IllegalArgumentException("Uneven string length in hex encoding.");
     60 
     61 		byte decoded[] = new byte[hex.length() / 2];
     62 
     63 		for (int i = 0; i < decoded.length; i++)
     64 		{
     65 			int hi = hexToInt(hex.charAt(i * 2));
     66 			int lo = hexToInt(hex.charAt((i * 2) + 1));
     67 
     68 			decoded[i] = (byte) (hi * 16 + lo);
     69 		}
     70 
     71 		return decoded;
     72 	}
     73 
     74 	private static byte[] generateKeyFromPasswordSaltWithMD5(byte[] password, byte[] salt, int keyLen)
     75 			throws IOException
     76 	{
     77 		if (salt.length < 8)
     78 			throw new IllegalArgumentException("Salt needs to be at least 8 bytes for key generation.");
     79 
     80 		MD5 md5 = new MD5();
     81 
     82 		byte[] key = new byte[keyLen];
     83 		byte[] tmp = new byte[md5.getDigestLength()];
     84 
     85 		while (true)
     86 		{
     87 			md5.update(password, 0, password.length);
     88 			md5.update(salt, 0, 8); // ARGH we only use the first 8 bytes of the salt in this step.
     89 			// This took me two hours until I got AES-xxx running.
     90 
     91 			int copy = (keyLen < tmp.length) ? keyLen : tmp.length;
     92 
     93 			md5.digest(tmp, 0);
     94 
     95 			System.arraycopy(tmp, 0, key, key.length - keyLen, copy);
     96 
     97 			keyLen -= copy;
     98 
     99 			if (keyLen == 0)
    100 				return key;
    101 
    102 			md5.update(tmp, 0, tmp.length);
    103 		}
    104 	}
    105 
    106 	private static byte[] removePadding(byte[] buff, int blockSize) throws IOException
    107 	{
    108 		/* Removes RFC 1423/PKCS #7 padding */
    109 
    110 		int rfc_1423_padding = buff[buff.length - 1] & 0xff;
    111 
    112 		if ((rfc_1423_padding < 1) || (rfc_1423_padding > blockSize))
    113 			throw new PEMDecryptException("Decrypted PEM has wrong padding, did you specify the correct password?");
    114 
    115 		for (int i = 2; i <= rfc_1423_padding; i++)
    116 		{
    117 			if (buff[buff.length - i] != rfc_1423_padding)
    118 				throw new PEMDecryptException("Decrypted PEM has wrong padding, did you specify the correct password?");
    119 		}
    120 
    121 		byte[] tmp = new byte[buff.length - rfc_1423_padding];
    122 		System.arraycopy(buff, 0, tmp, 0, buff.length - rfc_1423_padding);
    123 		return tmp;
    124 	}
    125 
    126 	private static PEMStructure parsePEM(char[] pem) throws IOException
    127 	{
    128 		PEMStructure ps = new PEMStructure();
    129 
    130 		String line = null;
    131 
    132 		BufferedReader br = new BufferedReader(new CharArrayReader(pem));
    133 
    134 		String endLine = null;
    135 
    136 		while (true)
    137 		{
    138 			line = br.readLine();
    139 
    140 			if (line == null)
    141 				throw new IOException("Invalid PEM structure, '-----BEGIN...' missing");
    142 
    143 			line = line.trim();
    144 
    145 			if (line.startsWith("-----BEGIN DSA PRIVATE KEY-----"))
    146 			{
    147 				endLine = "-----END DSA PRIVATE KEY-----";
    148 				ps.pemType = PEM_DSA_PRIVATE_KEY;
    149 				break;
    150 			}
    151 
    152 			if (line.startsWith("-----BEGIN RSA PRIVATE KEY-----"))
    153 			{
    154 				endLine = "-----END RSA PRIVATE KEY-----";
    155 				ps.pemType = PEM_RSA_PRIVATE_KEY;
    156 				break;
    157 			}
    158 		}
    159 
    160 		while (true)
    161 		{
    162 			line = br.readLine();
    163 
    164 			if (line == null)
    165 				throw new IOException("Invalid PEM structure, " + endLine + " missing");
    166 
    167 			line = line.trim();
    168 
    169 			int sem_idx = line.indexOf(':');
    170 
    171 			if (sem_idx == -1)
    172 				break;
    173 
    174 			String name = line.substring(0, sem_idx + 1);
    175 			String value = line.substring(sem_idx + 1);
    176 
    177 			String values[] = value.split(",");
    178 
    179 			for (int i = 0; i < values.length; i++)
    180 				values[i] = values[i].trim();
    181 
    182 			// Proc-Type: 4,ENCRYPTED
    183 			// DEK-Info: DES-EDE3-CBC,579B6BE3E5C60483
    184 
    185 			if ("Proc-Type:".equals(name))
    186 			{
    187 				ps.procType = values;
    188 				continue;
    189 			}
    190 
    191 			if ("DEK-Info:".equals(name))
    192 			{
    193 				ps.dekInfo = values;
    194 				continue;
    195 			}
    196 			/* Ignore line */
    197 		}
    198 
    199 		StringBuilder keyData = new StringBuilder();
    200 
    201 		while (true)
    202 		{
    203 			if (line == null)
    204 				throw new IOException("Invalid PEM structure, " + endLine + " missing");
    205 
    206 			line = line.trim();
    207 
    208 			if (line.startsWith(endLine))
    209 				break;
    210 
    211 			keyData.append(line);
    212 
    213 			line = br.readLine();
    214 		}
    215 
    216 		char[] pem_chars = new char[keyData.length()];
    217 		keyData.getChars(0, pem_chars.length, pem_chars, 0);
    218 
    219 		ps.data = Base64.decode(pem_chars);
    220 
    221 		if (ps.data.length == 0)
    222 			throw new IOException("Invalid PEM structure, no data available");
    223 
    224 		return ps;
    225 	}
    226 
    227 	private static void decryptPEM(PEMStructure ps, byte[] pw) throws IOException
    228 	{
    229 		if (ps.dekInfo == null)
    230 			throw new IOException("Broken PEM, no mode and salt given, but encryption enabled");
    231 
    232 		if (ps.dekInfo.length != 2)
    233 			throw new IOException("Broken PEM, DEK-Info is incomplete!");
    234 
    235 		String algo = ps.dekInfo[0];
    236 		byte[] salt = hexToByteArray(ps.dekInfo[1]);
    237 
    238 		BlockCipher bc = null;
    239 
    240 		if (algo.equals("DES-EDE3-CBC"))
    241 		{
    242 			DESede des3 = new DESede();
    243 			des3.init(false, generateKeyFromPasswordSaltWithMD5(pw, salt, 24));
    244 			bc = new CBCMode(des3, salt, false);
    245 		}
    246 		else if (algo.equals("DES-CBC"))
    247 		{
    248 			DES des = new DES();
    249 			des.init(false, generateKeyFromPasswordSaltWithMD5(pw, salt, 8));
    250 			bc = new CBCMode(des, salt, false);
    251 		}
    252 		else if (algo.equals("AES-128-CBC"))
    253 		{
    254 			AES aes = new AES();
    255 			aes.init(false, generateKeyFromPasswordSaltWithMD5(pw, salt, 16));
    256 			bc = new CBCMode(aes, salt, false);
    257 		}
    258 		else if (algo.equals("AES-192-CBC"))
    259 		{
    260 			AES aes = new AES();
    261 			aes.init(false, generateKeyFromPasswordSaltWithMD5(pw, salt, 24));
    262 			bc = new CBCMode(aes, salt, false);
    263 		}
    264 		else if (algo.equals("AES-256-CBC"))
    265 		{
    266 			AES aes = new AES();
    267 			aes.init(false, generateKeyFromPasswordSaltWithMD5(pw, salt, 32));
    268 			bc = new CBCMode(aes, salt, false);
    269 		}
    270 		else
    271 		{
    272 			throw new IOException("Cannot decrypt PEM structure, unknown cipher " + algo);
    273 		}
    274 
    275 		if ((ps.data.length % bc.getBlockSize()) != 0)
    276 			throw new IOException("Invalid PEM structure, size of encrypted block is not a multiple of "
    277 					+ bc.getBlockSize());
    278 
    279 		/* Now decrypt the content */
    280 
    281 		byte[] dz = new byte[ps.data.length];
    282 
    283 		for (int i = 0; i < ps.data.length / bc.getBlockSize(); i++)
    284 		{
    285 			bc.transformBlock(ps.data, i * bc.getBlockSize(), dz, i * bc.getBlockSize());
    286 		}
    287 
    288 		/* Now check and remove RFC 1423/PKCS #7 padding */
    289 
    290 		dz = removePadding(dz, bc.getBlockSize());
    291 
    292 		ps.data = dz;
    293 		ps.dekInfo = null;
    294 		ps.procType = null;
    295 	}
    296 
    297 	public static final boolean isPEMEncrypted(PEMStructure ps) throws IOException
    298 	{
    299 		if (ps.procType == null)
    300 			return false;
    301 
    302 		if (ps.procType.length != 2)
    303 			throw new IOException("Unknown Proc-Type field.");
    304 
    305 		if ("4".equals(ps.procType[0]) == false)
    306 			throw new IOException("Unknown Proc-Type field (" + ps.procType[0] + ")");
    307 
    308 		if ("ENCRYPTED".equals(ps.procType[1]))
    309 			return true;
    310 
    311 		return false;
    312 	}
    313 
    314 	public static final boolean isPEMEncrypted(char[] pem) throws IOException
    315 	{
    316 		return isPEMEncrypted(parsePEM(pem));
    317 	}
    318 
    319 	public static Object decode(char[] pem, String password) throws IOException
    320 	{
    321 		PEMStructure ps = parsePEM(pem);
    322 
    323 		if (isPEMEncrypted(ps))
    324 		{
    325 			if (password == null)
    326 				throw new IOException("PEM is encrypted, but no password was specified");
    327 
    328 			decryptPEM(ps, StringEncoder.GetBytes(password));
    329 		}
    330 
    331 		if (ps.pemType == PEM_DSA_PRIVATE_KEY)
    332 		{
    333 			SimpleDERReader dr = new SimpleDERReader(ps.data);
    334 
    335 			byte[] seq = dr.readSequenceAsByteArray();
    336 
    337 			if (dr.available() != 0)
    338 				throw new IOException("Padding in DSA PRIVATE KEY DER stream.");
    339 
    340 			dr.resetInput(seq);
    341 
    342 			BigInteger version = dr.readInt();
    343 
    344 			if (version.compareTo(BigInteger.ZERO) != 0)
    345 				throw new IOException("Wrong version (" + version + ") in DSA PRIVATE KEY DER stream.");
    346 
    347 			BigInteger p = dr.readInt();
    348 			BigInteger q = dr.readInt();
    349 			BigInteger g = dr.readInt();
    350 			BigInteger y = dr.readInt();
    351 			BigInteger x = dr.readInt();
    352 
    353 			if (dr.available() != 0)
    354 				throw new IOException("Padding in DSA PRIVATE KEY DER stream.");
    355 
    356 			return new DSAPrivateKey(p, q, g, y, x);
    357 		}
    358 
    359 		if (ps.pemType == PEM_RSA_PRIVATE_KEY)
    360 		{
    361 			SimpleDERReader dr = new SimpleDERReader(ps.data);
    362 
    363 			byte[] seq = dr.readSequenceAsByteArray();
    364 
    365 			if (dr.available() != 0)
    366 				throw new IOException("Padding in RSA PRIVATE KEY DER stream.");
    367 
    368 			dr.resetInput(seq);
    369 
    370 			BigInteger version = dr.readInt();
    371 
    372 			if ((version.compareTo(BigInteger.ZERO) != 0) && (version.compareTo(BigInteger.ONE) != 0))
    373 				throw new IOException("Wrong version (" + version + ") in RSA PRIVATE KEY DER stream.");
    374 
    375 			BigInteger n = dr.readInt();
    376 			BigInteger e = dr.readInt();
    377 			BigInteger d = dr.readInt();
    378 
    379 			return new RSAPrivateKey(d, e, n);
    380 		}
    381 
    382 		throw new IOException("PEM problem: it is of unknown type");
    383 	}
    384 
    385 }
    386