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