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