Mercurial > 510Connectbot
comparison src/com/trilead/ssh2/crypto/PEMDecoder.java @ 0:0ce5cc452d02
initial version
author | Carl Byington <carl@five-ten-sg.com> |
---|---|
date | Thu, 22 May 2014 10:41:19 -0700 |
parents | |
children |
comparison
equal
deleted
inserted
replaced
-1:000000000000 | 0:0ce5cc452d02 |
---|---|
1 | |
2 package com.trilead.ssh2.crypto; | |
3 | |
4 import java.io.BufferedReader; | |
5 import java.io.CharArrayReader; | |
6 import java.io.IOException; | |
7 import java.math.BigInteger; | |
8 import java.security.DigestException; | |
9 import java.security.KeyFactory; | |
10 import java.security.KeyPair; | |
11 import java.security.MessageDigest; | |
12 import java.security.NoSuchAlgorithmException; | |
13 import java.security.PrivateKey; | |
14 import java.security.PublicKey; | |
15 import java.security.spec.DSAPrivateKeySpec; | |
16 import java.security.spec.DSAPublicKeySpec; | |
17 import java.security.spec.ECParameterSpec; | |
18 import java.security.spec.ECPoint; | |
19 import java.security.spec.ECPrivateKeySpec; | |
20 import java.security.spec.ECPublicKeySpec; | |
21 import java.security.spec.InvalidKeySpecException; | |
22 import java.security.spec.KeySpec; | |
23 import java.security.spec.RSAPrivateCrtKeySpec; | |
24 import java.security.spec.RSAPrivateKeySpec; | |
25 import java.security.spec.RSAPublicKeySpec; | |
26 | |
27 import com.trilead.ssh2.crypto.cipher.AES; | |
28 import com.trilead.ssh2.crypto.cipher.BlockCipher; | |
29 import com.trilead.ssh2.crypto.cipher.CBCMode; | |
30 import com.trilead.ssh2.crypto.cipher.DES; | |
31 import com.trilead.ssh2.crypto.cipher.DESede; | |
32 import com.trilead.ssh2.signature.ECDSASHA2Verify; | |
33 | |
34 /** | |
35 * PEM Support. | |
36 * | |
37 * @author Christian Plattner, plattner@trilead.com | |
38 * @version $Id: PEMDecoder.java,v 1.2 2008/04/01 12:38:09 cplattne Exp $ | |
39 */ | |
40 public class PEMDecoder { | |
41 public static final int PEM_RSA_PRIVATE_KEY = 1; | |
42 public static final int PEM_DSA_PRIVATE_KEY = 2; | |
43 public static final int PEM_EC_PRIVATE_KEY = 3; | |
44 | |
45 private static final int hexToInt(char c) { | |
46 if ((c >= 'a') && (c <= 'f')) { | |
47 return (c - 'a') + 10; | |
48 } | |
49 | |
50 if ((c >= 'A') && (c <= 'F')) { | |
51 return (c - 'A') + 10; | |
52 } | |
53 | |
54 if ((c >= '0') && (c <= '9')) { | |
55 return (c - '0'); | |
56 } | |
57 | |
58 throw new IllegalArgumentException("Need hex char"); | |
59 } | |
60 | |
61 private static byte[] hexToByteArray(String hex) { | |
62 if (hex == null) | |
63 throw new IllegalArgumentException("null argument"); | |
64 | |
65 if ((hex.length() % 2) != 0) | |
66 throw new IllegalArgumentException("Uneven string length in hex encoding."); | |
67 | |
68 byte decoded[] = new byte[hex.length() / 2]; | |
69 | |
70 for (int i = 0; i < decoded.length; i++) { | |
71 int hi = hexToInt(hex.charAt(i * 2)); | |
72 int lo = hexToInt(hex.charAt((i * 2) + 1)); | |
73 decoded[i] = (byte)(hi * 16 + lo); | |
74 } | |
75 | |
76 return decoded; | |
77 } | |
78 | |
79 private static byte[] generateKeyFromPasswordSaltWithMD5(byte[] password, byte[] salt, int keyLen) | |
80 throws IOException { | |
81 if (salt.length < 8) | |
82 throw new IllegalArgumentException("Salt needs to be at least 8 bytes for key generation."); | |
83 | |
84 MessageDigest md5; | |
85 | |
86 try { | |
87 md5 = MessageDigest.getInstance("MD5"); | |
88 } | |
89 catch (NoSuchAlgorithmException e) { | |
90 throw new IllegalArgumentException("VM does not support MD5", e); | |
91 } | |
92 | |
93 byte[] key = new byte[keyLen]; | |
94 byte[] tmp = new byte[md5.getDigestLength()]; | |
95 | |
96 while (true) { | |
97 md5.update(password, 0, password.length); | |
98 md5.update(salt, 0, 8); // ARGH we only use the first 8 bytes of the | |
99 // salt in this step. | |
100 // This took me two hours until I got AES-xxx running. | |
101 int copy = (keyLen < tmp.length) ? keyLen : tmp.length; | |
102 | |
103 try { | |
104 md5.digest(tmp, 0, tmp.length); | |
105 } | |
106 catch (DigestException e) { | |
107 IOException ex = new IOException("could not digest password"); | |
108 ex.initCause(e); | |
109 throw ex; | |
110 } | |
111 | |
112 System.arraycopy(tmp, 0, key, key.length - keyLen, copy); | |
113 keyLen -= copy; | |
114 | |
115 if (keyLen == 0) | |
116 return key; | |
117 | |
118 md5.update(tmp, 0, tmp.length); | |
119 } | |
120 } | |
121 | |
122 private static byte[] removePadding(byte[] buff, int blockSize) throws IOException { | |
123 /* Removes RFC 1423/PKCS #7 padding */ | |
124 int rfc_1423_padding = buff[buff.length - 1] & 0xff; | |
125 | |
126 if ((rfc_1423_padding < 1) || (rfc_1423_padding > blockSize)) | |
127 throw new IOException("Decrypted PEM has wrong padding, did you specify the correct password?"); | |
128 | |
129 for (int i = 2; i <= rfc_1423_padding; i++) { | |
130 if (buff[buff.length - i] != rfc_1423_padding) | |
131 throw new IOException("Decrypted PEM has wrong padding, did you specify the correct password?"); | |
132 } | |
133 | |
134 byte[] tmp = new byte[buff.length - rfc_1423_padding]; | |
135 System.arraycopy(buff, 0, tmp, 0, buff.length - rfc_1423_padding); | |
136 return tmp; | |
137 } | |
138 | |
139 public static final PEMStructure parsePEM(char[] pem) throws IOException { | |
140 PEMStructure ps = new PEMStructure(); | |
141 String line = null; | |
142 BufferedReader br = new BufferedReader(new CharArrayReader(pem)); | |
143 String endLine = null; | |
144 | |
145 while (true) { | |
146 line = br.readLine(); | |
147 | |
148 if (line == null) | |
149 throw new IOException("Invalid PEM structure, '-----BEGIN...' missing"); | |
150 | |
151 line = line.trim(); | |
152 | |
153 if (line.startsWith("-----BEGIN DSA PRIVATE KEY-----")) { | |
154 endLine = "-----END DSA PRIVATE KEY-----"; | |
155 ps.pemType = PEM_DSA_PRIVATE_KEY; | |
156 break; | |
157 } | |
158 | |
159 if (line.startsWith("-----BEGIN RSA PRIVATE KEY-----")) { | |
160 endLine = "-----END RSA PRIVATE KEY-----"; | |
161 ps.pemType = PEM_RSA_PRIVATE_KEY; | |
162 break; | |
163 } | |
164 | |
165 if (line.startsWith("-----BEGIN EC PRIVATE KEY-----")) { | |
166 endLine = "-----END EC PRIVATE KEY-----"; | |
167 ps.pemType = PEM_EC_PRIVATE_KEY; | |
168 break; | |
169 } | |
170 } | |
171 | |
172 while (true) { | |
173 line = br.readLine(); | |
174 | |
175 if (line == null) | |
176 throw new IOException("Invalid PEM structure, " + endLine + " missing"); | |
177 | |
178 line = line.trim(); | |
179 int sem_idx = line.indexOf(':'); | |
180 | |
181 if (sem_idx == -1) | |
182 break; | |
183 | |
184 String name = line.substring(0, sem_idx + 1); | |
185 String value = line.substring(sem_idx + 1); | |
186 String values[] = value.split(","); | |
187 | |
188 for (int i = 0; i < values.length; i++) | |
189 values[i] = values[i].trim(); | |
190 | |
191 // Proc-Type: 4,ENCRYPTED | |
192 // DEK-Info: DES-EDE3-CBC,579B6BE3E5C60483 | |
193 | |
194 if ("Proc-Type:".equals(name)) { | |
195 ps.procType = values; | |
196 continue; | |
197 } | |
198 | |
199 if ("DEK-Info:".equals(name)) { | |
200 ps.dekInfo = values; | |
201 continue; | |
202 } | |
203 | |
204 /* Ignore line */ | |
205 } | |
206 | |
207 StringBuffer keyData = new StringBuffer(); | |
208 | |
209 while (true) { | |
210 if (line == null) | |
211 throw new IOException("Invalid PEM structure, " + endLine + " missing"); | |
212 | |
213 line = line.trim(); | |
214 | |
215 if (line.startsWith(endLine)) | |
216 break; | |
217 | |
218 keyData.append(line); | |
219 line = br.readLine(); | |
220 } | |
221 | |
222 char[] pem_chars = new char[keyData.length()]; | |
223 keyData.getChars(0, pem_chars.length, pem_chars, 0); | |
224 ps.data = Base64.decode(pem_chars); | |
225 | |
226 if (ps.data.length == 0) | |
227 throw new IOException("Invalid PEM structure, no data available"); | |
228 | |
229 return ps; | |
230 } | |
231 | |
232 private static final void decryptPEM(PEMStructure ps, byte[] pw) throws IOException { | |
233 if (ps.dekInfo == null) | |
234 throw new IOException("Broken PEM, no mode and salt given, but encryption enabled"); | |
235 | |
236 if (ps.dekInfo.length != 2) | |
237 throw new IOException("Broken PEM, DEK-Info is incomplete!"); | |
238 | |
239 String algo = ps.dekInfo[0]; | |
240 byte[] salt = hexToByteArray(ps.dekInfo[1]); | |
241 BlockCipher bc = null; | |
242 | |
243 if (algo.equals("DES-EDE3-CBC")) { | |
244 DESede des3 = new DESede(); | |
245 des3.init(false, generateKeyFromPasswordSaltWithMD5(pw, salt, 24)); | |
246 bc = new CBCMode(des3, salt, false); | |
247 } | |
248 else if (algo.equals("DES-CBC")) { | |
249 DES des = new DES(); | |
250 des.init(false, generateKeyFromPasswordSaltWithMD5(pw, salt, 8)); | |
251 bc = new CBCMode(des, salt, false); | |
252 } | |
253 else if (algo.equals("AES-128-CBC")) { | |
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 AES aes = new AES(); | |
260 aes.init(false, generateKeyFromPasswordSaltWithMD5(pw, salt, 24)); | |
261 bc = new CBCMode(aes, salt, false); | |
262 } | |
263 else if (algo.equals("AES-256-CBC")) { | |
264 AES aes = new AES(); | |
265 aes.init(false, generateKeyFromPasswordSaltWithMD5(pw, salt, 32)); | |
266 bc = new CBCMode(aes, salt, false); | |
267 } | |
268 else { | |
269 throw new IOException("Cannot decrypt PEM structure, unknown cipher " + algo); | |
270 } | |
271 | |
272 if ((ps.data.length % bc.getBlockSize()) != 0) | |
273 throw new IOException("Invalid PEM structure, size of encrypted block is not a multiple of " | |
274 + bc.getBlockSize()); | |
275 | |
276 /* Now decrypt the content */ | |
277 byte[] dz = new byte[ps.data.length]; | |
278 | |
279 for (int i = 0; i < ps.data.length / bc.getBlockSize(); i++) { | |
280 bc.transformBlock(ps.data, i * bc.getBlockSize(), dz, i * bc.getBlockSize()); | |
281 } | |
282 | |
283 /* Now check and remove RFC 1423/PKCS #7 padding */ | |
284 dz = removePadding(dz, bc.getBlockSize()); | |
285 ps.data = dz; | |
286 ps.dekInfo = null; | |
287 ps.procType = null; | |
288 } | |
289 | |
290 public static final boolean isPEMEncrypted(PEMStructure ps) throws IOException { | |
291 if (ps.procType == null) | |
292 return false; | |
293 | |
294 if (ps.procType.length != 2) | |
295 throw new IOException("Unknown Proc-Type field."); | |
296 | |
297 if ("4".equals(ps.procType[0]) == false) | |
298 throw new IOException("Unknown Proc-Type field (" + ps.procType[0] + ")"); | |
299 | |
300 if ("ENCRYPTED".equals(ps.procType[1])) | |
301 return true; | |
302 | |
303 return false; | |
304 } | |
305 | |
306 public static KeyPair decode(char[] pem, String password) throws IOException { | |
307 PEMStructure ps = parsePEM(pem); | |
308 return decode(ps, password); | |
309 } | |
310 | |
311 public static KeyPair decode(PEMStructure ps, String password) throws IOException { | |
312 if (isPEMEncrypted(ps)) { | |
313 if (password == null) | |
314 throw new IOException("PEM is encrypted, but no password was specified"); | |
315 | |
316 decryptPEM(ps, password.getBytes("ISO-8859-1")); | |
317 } | |
318 | |
319 if (ps.pemType == PEM_DSA_PRIVATE_KEY) { | |
320 SimpleDERReader dr = new SimpleDERReader(ps.data); | |
321 byte[] seq = dr.readSequenceAsByteArray(); | |
322 | |
323 if (dr.available() != 0) | |
324 throw new IOException("Padding in DSA PRIVATE KEY DER stream."); | |
325 | |
326 dr.resetInput(seq); | |
327 BigInteger version = dr.readInt(); | |
328 | |
329 if (version.compareTo(BigInteger.ZERO) != 0) | |
330 throw new IOException("Wrong version (" + version + ") in DSA PRIVATE KEY DER stream."); | |
331 | |
332 BigInteger p = dr.readInt(); | |
333 BigInteger q = dr.readInt(); | |
334 BigInteger g = dr.readInt(); | |
335 BigInteger y = dr.readInt(); | |
336 BigInteger x = dr.readInt(); | |
337 | |
338 if (dr.available() != 0) | |
339 throw new IOException("Padding in DSA PRIVATE KEY DER stream."); | |
340 | |
341 DSAPrivateKeySpec privSpec = new DSAPrivateKeySpec(x, p, q, g); | |
342 DSAPublicKeySpec pubSpec = new DSAPublicKeySpec(y, p, q, g); | |
343 return generateKeyPair("DSA", privSpec, pubSpec); | |
344 } | |
345 | |
346 if (ps.pemType == PEM_RSA_PRIVATE_KEY) { | |
347 SimpleDERReader dr = new SimpleDERReader(ps.data); | |
348 byte[] seq = dr.readSequenceAsByteArray(); | |
349 | |
350 if (dr.available() != 0) | |
351 throw new IOException("Padding in RSA PRIVATE KEY DER stream."); | |
352 | |
353 dr.resetInput(seq); | |
354 BigInteger version = dr.readInt(); | |
355 | |
356 if ((version.compareTo(BigInteger.ZERO) != 0) && (version.compareTo(BigInteger.ONE) != 0)) | |
357 throw new IOException("Wrong version (" + version + ") in RSA PRIVATE KEY DER stream."); | |
358 | |
359 BigInteger n = dr.readInt(); | |
360 BigInteger e = dr.readInt(); | |
361 BigInteger d = dr.readInt(); | |
362 // TODO: is this right? | |
363 BigInteger primeP = dr.readInt(); | |
364 BigInteger primeQ = dr.readInt(); | |
365 BigInteger expP = dr.readInt(); | |
366 BigInteger expQ = dr.readInt(); | |
367 BigInteger coeff = dr.readInt(); | |
368 RSAPrivateKeySpec privSpec = new RSAPrivateCrtKeySpec(n, e, d, primeP, primeQ, expP, expQ, coeff); | |
369 RSAPublicKeySpec pubSpec = new RSAPublicKeySpec(n, e); | |
370 return generateKeyPair("RSA", privSpec, pubSpec); | |
371 } | |
372 | |
373 if (ps.pemType == PEM_EC_PRIVATE_KEY) { | |
374 SimpleDERReader dr = new SimpleDERReader(ps.data); | |
375 byte[] seq = dr.readSequenceAsByteArray(); | |
376 | |
377 if (dr.available() != 0) | |
378 throw new IOException("Padding in EC PRIVATE KEY DER stream."); | |
379 | |
380 dr.resetInput(seq); | |
381 BigInteger version = dr.readInt(); | |
382 | |
383 if ((version.compareTo(BigInteger.ONE) != 0)) | |
384 throw new IOException("Wrong version (" + version + ") in EC PRIVATE KEY DER stream."); | |
385 | |
386 byte[] privateBytes = dr.readOctetString(); | |
387 String curveOid = null; | |
388 byte[] publicBytes = null; | |
389 | |
390 while (dr.available() > 0) { | |
391 int type = dr.readConstructedType(); | |
392 SimpleDERReader cr = dr.readConstructed(); | |
393 | |
394 switch (type) { | |
395 case 0: | |
396 curveOid = cr.readOid(); | |
397 break; | |
398 | |
399 case 1: | |
400 publicBytes = cr.readOctetString(); | |
401 break; | |
402 } | |
403 } | |
404 | |
405 ECParameterSpec params = ECDSASHA2Verify.getCurveForOID(curveOid); | |
406 | |
407 if (params == null) | |
408 throw new IOException("invalid OID"); | |
409 | |
410 BigInteger s = new BigInteger(privateBytes); | |
411 byte[] publicBytesSlice = new byte[publicBytes.length - 1]; | |
412 System.arraycopy(publicBytes, 1, publicBytesSlice, 0, publicBytesSlice.length); | |
413 ECPoint w = ECDSASHA2Verify.decodeECPoint(publicBytesSlice, params.getCurve()); | |
414 ECPrivateKeySpec privSpec = new ECPrivateKeySpec(s, params); | |
415 ECPublicKeySpec pubSpec = new ECPublicKeySpec(w, params); | |
416 return generateKeyPair("EC", privSpec, pubSpec); | |
417 } | |
418 | |
419 throw new IOException("PEM problem: it is of unknown type"); | |
420 } | |
421 | |
422 /** | |
423 * Generate a {@code KeyPair} given an {@code algorithm} and {@code KeySpec}. | |
424 */ | |
425 private static KeyPair generateKeyPair(String algorithm, KeySpec privSpec, KeySpec pubSpec) | |
426 throws IOException { | |
427 try { | |
428 final KeyFactory kf = KeyFactory.getInstance(algorithm); | |
429 final PublicKey pubKey = kf.generatePublic(pubSpec); | |
430 final PrivateKey privKey = kf.generatePrivate(privSpec); | |
431 return new KeyPair(pubKey, privKey); | |
432 } | |
433 catch (NoSuchAlgorithmException ex) { | |
434 IOException ioex = new IOException(); | |
435 ioex.initCause(ex); | |
436 throw ioex; | |
437 } | |
438 catch (InvalidKeySpecException ex) { | |
439 IOException ioex = new IOException("invalid keyspec"); | |
440 ioex.initCause(ex); | |
441 throw ioex; | |
442 } | |
443 } | |
444 } |