| |
| package com.trilead.ssh2.crypto; |
| |
| import com.trilead.ssh2.crypto.cipher.*; |
| import com.trilead.ssh2.crypto.digest.MD5; |
| import com.trilead.ssh2.signature.DSAPrivateKey; |
| import com.trilead.ssh2.signature.RSAPrivateKey; |
| |
| import java.io.BufferedReader; |
| import java.io.CharArrayReader; |
| import java.io.IOException; |
| import java.math.BigInteger; |
| |
| /** |
| * PEM Support. |
| * |
| * @author Christian Plattner, plattner@trilead.com |
| * @version $Id: PEMDecoder.java,v 1.2 2008/04/01 12:38:09 cplattne Exp $ |
| */ |
| public class PEMDecoder |
| { |
| private static final int PEM_RSA_PRIVATE_KEY = 1; |
| private static final int PEM_DSA_PRIVATE_KEY = 2; |
| |
| private static final int hexToInt(char c) |
| { |
| if ((c >= 'a') && (c <= 'f')) |
| { |
| return (c - 'a') + 10; |
| } |
| |
| if ((c >= 'A') && (c <= 'F')) |
| { |
| return (c - 'A') + 10; |
| } |
| |
| if ((c >= '0') && (c <= '9')) |
| { |
| return (c - '0'); |
| } |
| |
| throw new IllegalArgumentException("Need hex char"); |
| } |
| |
| private static byte[] hexToByteArray(String hex) |
| { |
| if (hex == null) |
| throw new IllegalArgumentException("null argument"); |
| |
| if ((hex.length() % 2) != 0) |
| throw new IllegalArgumentException("Uneven string length in hex encoding."); |
| |
| byte decoded[] = new byte[hex.length() / 2]; |
| |
| for (int i = 0; i < decoded.length; i++) |
| { |
| int hi = hexToInt(hex.charAt(i * 2)); |
| int lo = hexToInt(hex.charAt((i * 2) + 1)); |
| |
| decoded[i] = (byte) (hi * 16 + lo); |
| } |
| |
| return decoded; |
| } |
| |
| private static byte[] generateKeyFromPasswordSaltWithMD5(byte[] password, byte[] salt, int keyLen) |
| throws IOException |
| { |
| if (salt.length < 8) |
| throw new IllegalArgumentException("Salt needs to be at least 8 bytes for key generation."); |
| |
| MD5 md5 = new MD5(); |
| |
| byte[] key = new byte[keyLen]; |
| byte[] tmp = new byte[md5.getDigestLength()]; |
| |
| while (true) |
| { |
| md5.update(password, 0, password.length); |
| md5.update(salt, 0, 8); // ARGH we only use the first 8 bytes of the |
| // salt in this step. |
| // This took me two hours until I got AES-xxx running. |
| |
| int copy = (keyLen < tmp.length) ? keyLen : tmp.length; |
| |
| md5.digest(tmp, 0); |
| |
| System.arraycopy(tmp, 0, key, key.length - keyLen, copy); |
| |
| keyLen -= copy; |
| |
| if (keyLen == 0) |
| return key; |
| |
| md5.update(tmp, 0, tmp.length); |
| } |
| } |
| |
| private static byte[] removePadding(byte[] buff, int blockSize) throws IOException |
| { |
| /* Removes RFC 1423/PKCS #7 padding */ |
| |
| int rfc_1423_padding = buff[buff.length - 1] & 0xff; |
| |
| if ((rfc_1423_padding < 1) || (rfc_1423_padding > blockSize)) |
| throw new IOException("Decrypted PEM has wrong padding, did you specify the correct password?"); |
| |
| for (int i = 2; i <= rfc_1423_padding; i++) |
| { |
| if (buff[buff.length - i] != rfc_1423_padding) |
| throw new IOException("Decrypted PEM has wrong padding, did you specify the correct password?"); |
| } |
| |
| byte[] tmp = new byte[buff.length - rfc_1423_padding]; |
| System.arraycopy(buff, 0, tmp, 0, buff.length - rfc_1423_padding); |
| return tmp; |
| } |
| |
| private static final PEMStructure parsePEM(char[] pem) throws IOException |
| { |
| PEMStructure ps = new PEMStructure(); |
| |
| String line = null; |
| |
| BufferedReader br = new BufferedReader(new CharArrayReader(pem)); |
| |
| String endLine = null; |
| |
| while (true) |
| { |
| line = br.readLine(); |
| |
| if (line == null) |
| throw new IOException("Invalid PEM structure, '-----BEGIN...' missing"); |
| |
| line = line.trim(); |
| |
| if (line.startsWith("-----BEGIN DSA PRIVATE KEY-----")) |
| { |
| endLine = "-----END DSA PRIVATE KEY-----"; |
| ps.pemType = PEM_DSA_PRIVATE_KEY; |
| break; |
| } |
| |
| if (line.startsWith("-----BEGIN RSA PRIVATE KEY-----")) |
| { |
| endLine = "-----END RSA PRIVATE KEY-----"; |
| ps.pemType = PEM_RSA_PRIVATE_KEY; |
| break; |
| } |
| } |
| |
| while (true) |
| { |
| line = br.readLine(); |
| |
| if (line == null) |
| throw new IOException("Invalid PEM structure, " + endLine + " missing"); |
| |
| line = line.trim(); |
| |
| int sem_idx = line.indexOf(':'); |
| |
| if (sem_idx == -1) |
| break; |
| |
| String name = line.substring(0, sem_idx + 1); |
| String value = line.substring(sem_idx + 1); |
| |
| String values[] = value.split(","); |
| |
| for (int i = 0; i < values.length; i++) |
| values[i] = values[i].trim(); |
| |
| // Proc-Type: 4,ENCRYPTED |
| // DEK-Info: DES-EDE3-CBC,579B6BE3E5C60483 |
| |
| if ("Proc-Type:".equals(name)) |
| { |
| ps.procType = values; |
| continue; |
| } |
| |
| if ("DEK-Info:".equals(name)) |
| { |
| ps.dekInfo = values; |
| continue; |
| } |
| /* Ignore line */ |
| } |
| |
| StringBuffer keyData = new StringBuffer(); |
| |
| while (true) |
| { |
| if (line == null) |
| throw new IOException("Invalid PEM structure, " + endLine + " missing"); |
| |
| line = line.trim(); |
| |
| if (line.startsWith(endLine)) |
| break; |
| |
| keyData.append(line); |
| |
| line = br.readLine(); |
| } |
| |
| char[] pem_chars = new char[keyData.length()]; |
| keyData.getChars(0, pem_chars.length, pem_chars, 0); |
| |
| ps.data = Base64.decode(pem_chars); |
| |
| if (ps.data.length == 0) |
| throw new IOException("Invalid PEM structure, no data available"); |
| |
| return ps; |
| } |
| |
| private static final void decryptPEM(PEMStructure ps, byte[] pw) throws IOException |
| { |
| if (ps.dekInfo == null) |
| throw new IOException("Broken PEM, no mode and salt given, but encryption enabled"); |
| |
| if (ps.dekInfo.length != 2) |
| throw new IOException("Broken PEM, DEK-Info is incomplete!"); |
| |
| String algo = ps.dekInfo[0]; |
| byte[] salt = hexToByteArray(ps.dekInfo[1]); |
| |
| BlockCipher bc = null; |
| |
| if (algo.equals("DES-EDE3-CBC")) |
| { |
| DESede des3 = new DESede(); |
| des3.init(false, generateKeyFromPasswordSaltWithMD5(pw, salt, 24)); |
| bc = new CBCMode(des3, salt, false); |
| } |
| else if (algo.equals("DES-CBC")) |
| { |
| DES des = new DES(); |
| des.init(false, generateKeyFromPasswordSaltWithMD5(pw, salt, 8)); |
| bc = new CBCMode(des, salt, false); |
| } |
| else if (algo.equals("AES-128-CBC")) |
| { |
| AES aes = new AES(); |
| aes.init(false, generateKeyFromPasswordSaltWithMD5(pw, salt, 16)); |
| bc = new CBCMode(aes, salt, false); |
| } |
| else if (algo.equals("AES-192-CBC")) |
| { |
| AES aes = new AES(); |
| aes.init(false, generateKeyFromPasswordSaltWithMD5(pw, salt, 24)); |
| bc = new CBCMode(aes, salt, false); |
| } |
| else if (algo.equals("AES-256-CBC")) |
| { |
| AES aes = new AES(); |
| aes.init(false, generateKeyFromPasswordSaltWithMD5(pw, salt, 32)); |
| bc = new CBCMode(aes, salt, false); |
| } |
| else |
| { |
| throw new IOException("Cannot decrypt PEM structure, unknown cipher " + algo); |
| } |
| |
| if ((ps.data.length % bc.getBlockSize()) != 0) |
| throw new IOException("Invalid PEM structure, size of encrypted block is not a multiple of " |
| + bc.getBlockSize()); |
| |
| /* Now decrypt the content */ |
| |
| byte[] dz = new byte[ps.data.length]; |
| |
| for (int i = 0; i < ps.data.length / bc.getBlockSize(); i++) |
| { |
| bc.transformBlock(ps.data, i * bc.getBlockSize(), dz, i * bc.getBlockSize()); |
| } |
| |
| /* Now check and remove RFC 1423/PKCS #7 padding */ |
| |
| dz = removePadding(dz, bc.getBlockSize()); |
| |
| ps.data = dz; |
| ps.dekInfo = null; |
| ps.procType = null; |
| } |
| |
| public static final boolean isPEMEncrypted(PEMStructure ps) throws IOException |
| { |
| if (ps.procType == null) |
| return false; |
| |
| if (ps.procType.length != 2) |
| throw new IOException("Unknown Proc-Type field."); |
| |
| if ("4".equals(ps.procType[0]) == false) |
| throw new IOException("Unknown Proc-Type field (" + ps.procType[0] + ")"); |
| |
| if ("ENCRYPTED".equals(ps.procType[1])) |
| return true; |
| |
| return false; |
| } |
| |
| public static Object decode(char[] pem, String password) throws IOException |
| { |
| PEMStructure ps = parsePEM(pem); |
| |
| if (isPEMEncrypted(ps)) |
| { |
| if (password == null) |
| throw new IOException("PEM is encrypted, but no password was specified"); |
| |
| decryptPEM(ps, password.getBytes("ISO-8859-1")); |
| } |
| |
| if (ps.pemType == PEM_DSA_PRIVATE_KEY) |
| { |
| SimpleDERReader dr = new SimpleDERReader(ps.data); |
| |
| byte[] seq = dr.readSequenceAsByteArray(); |
| |
| if (dr.available() != 0) |
| throw new IOException("Padding in DSA PRIVATE KEY DER stream."); |
| |
| dr.resetInput(seq); |
| |
| BigInteger version = dr.readInt(); |
| |
| if (version.compareTo(BigInteger.ZERO) != 0) |
| throw new IOException("Wrong version (" + version + ") in DSA PRIVATE KEY DER stream."); |
| |
| BigInteger p = dr.readInt(); |
| BigInteger q = dr.readInt(); |
| BigInteger g = dr.readInt(); |
| BigInteger y = dr.readInt(); |
| BigInteger x = dr.readInt(); |
| |
| if (dr.available() != 0) |
| throw new IOException("Padding in DSA PRIVATE KEY DER stream."); |
| |
| return new DSAPrivateKey(p, q, g, y, x); |
| } |
| |
| if (ps.pemType == PEM_RSA_PRIVATE_KEY) |
| { |
| SimpleDERReader dr = new SimpleDERReader(ps.data); |
| |
| byte[] seq = dr.readSequenceAsByteArray(); |
| |
| if (dr.available() != 0) |
| throw new IOException("Padding in RSA PRIVATE KEY DER stream."); |
| |
| dr.resetInput(seq); |
| |
| BigInteger version = dr.readInt(); |
| |
| if ((version.compareTo(BigInteger.ZERO) != 0) && (version.compareTo(BigInteger.ONE) != 0)) |
| throw new IOException("Wrong version (" + version + ") in RSA PRIVATE KEY DER stream."); |
| |
| BigInteger n = dr.readInt(); |
| BigInteger e = dr.readInt(); |
| BigInteger d = dr.readInt(); |
| |
| return new RSAPrivateKey(d, e, n); |
| } |
| |
| throw new IOException("PEM problem: it is of unknown type"); |
| } |
| |
| } |