Commit | Line | Data |
---|---|---|
52e0732e NR |
1 | package be.nikiroo.utils; |
2 | ||
3 | import java.io.IOException; | |
d20c8d77 NR |
4 | import java.io.InputStream; |
5 | import java.io.OutputStream; | |
9fb03c36 | 6 | import java.io.UnsupportedEncodingException; |
12784931 | 7 | import java.security.InvalidAlgorithmParameterException; |
52e0732e NR |
8 | import java.security.InvalidKeyException; |
9 | import java.security.NoSuchAlgorithmException; | |
10 | ||
11 | import javax.crypto.BadPaddingException; | |
12 | import javax.crypto.Cipher; | |
d20c8d77 NR |
13 | import javax.crypto.CipherInputStream; |
14 | import javax.crypto.CipherOutputStream; | |
52e0732e NR |
15 | import javax.crypto.IllegalBlockSizeException; |
16 | import javax.crypto.NoSuchPaddingException; | |
17 | import javax.crypto.SecretKey; | |
12784931 | 18 | import javax.crypto.spec.IvParameterSpec; |
52e0732e | 19 | import javax.crypto.spec.SecretKeySpec; |
9fb03c36 | 20 | import javax.net.ssl.SSLException; |
52e0732e | 21 | |
a6a73de3 NR |
22 | import be.nikiroo.utils.streams.Base64InputStream; |
23 | import be.nikiroo.utils.streams.Base64OutputStream; | |
24 | ||
52e0732e NR |
25 | /** |
26 | * Small utility class to do AES encryption/decryption. | |
27 | * <p> | |
0747c3c2 NR |
28 | * For the moment, it is multi-thread compatible, but beware: |
29 | * <ul> | |
30 | * <li>The encrypt/decrypt calls are serialized</li> | |
31 | * <li>The streams are independent and thus parallel</li> | |
32 | * </ul> | |
33 | * <p> | |
52e0732e NR |
34 | * Do not assume it is actually secure until you checked the code... |
35 | * | |
36 | * @author niki | |
37 | */ | |
38 | public class CryptUtils { | |
12784931 NR |
39 | static private final String AES_NAME = "AES/CFB8/NoPadding"; |
40 | ||
52e0732e NR |
41 | private Cipher ecipher; |
42 | private Cipher dcipher; | |
0747c3c2 | 43 | private SecretKey key; |
52e0732e NR |
44 | |
45 | /** | |
12784931 NR |
46 | * Small and lazy-easy way to initialize a 128 bits key with |
47 | * {@link CryptUtils}. | |
52e0732e NR |
48 | * <p> |
49 | * <b>Some</b> part of the key will be used to generate a 128 bits key and | |
50 | * initialize the {@link CryptUtils}; even NULL will generate something. | |
51 | * <p> | |
52 | * <b>This is most probably not secure. Do not use if you actually care | |
53 | * about security.</b> | |
54 | * | |
55 | * @param key | |
56 | * the {@link String} to use as a base for the key, can be NULL | |
57 | */ | |
58 | public CryptUtils(String key) { | |
59 | try { | |
0747c3c2 | 60 | init(key2key(key)); |
52e0732e NR |
61 | } catch (InvalidKeyException e) { |
62 | // We made sure that the key is correct, so nothing here | |
63 | e.printStackTrace(); | |
64 | } | |
65 | } | |
66 | ||
67 | /** | |
68 | * Create a new instance of {@link CryptUtils} with the given 128 bytes key. | |
69 | * <p> | |
70 | * The key <b>must</b> be exactly 128 bytes long. | |
71 | * | |
72 | * @param bytes32 | |
73 | * the 128 bits (32 bytes) of the key | |
74 | * | |
75 | * @throws InvalidKeyException | |
76 | * if the key is not an array of 128 bytes | |
77 | */ | |
78 | public CryptUtils(byte[] bytes32) throws InvalidKeyException { | |
79 | init(bytes32); | |
80 | } | |
81 | ||
d20c8d77 NR |
82 | /** |
83 | * Wrap the given {@link InputStream} so it is transparently encrypted by | |
84 | * the current {@link CryptUtils}. | |
85 | * | |
86 | * @param in | |
87 | * the {@link InputStream} to wrap | |
88 | * @return the auto-encode {@link InputStream} | |
89 | */ | |
12784931 | 90 | public InputStream encrypt(InputStream in) { |
0747c3c2 | 91 | Cipher ecipher = newCipher(Cipher.ENCRYPT_MODE); |
d20c8d77 NR |
92 | return new CipherInputStream(in, ecipher); |
93 | } | |
94 | ||
12784931 NR |
95 | /** |
96 | * Wrap the given {@link InputStream} so it is transparently encrypted by | |
97 | * the current {@link CryptUtils} and encoded in base64. | |
98 | * | |
99 | * @param in | |
100 | * the {@link InputStream} to wrap | |
12784931 NR |
101 | * |
102 | * @return the auto-encode {@link InputStream} | |
103 | * | |
104 | * @throws IOException | |
105 | * in case of I/O error | |
106 | */ | |
a6a73de3 NR |
107 | public InputStream encrypt64(InputStream in) throws IOException { |
108 | return new Base64InputStream(encrypt(in), true); | |
12784931 NR |
109 | } |
110 | ||
d20c8d77 NR |
111 | /** |
112 | * Wrap the given {@link OutputStream} so it is transparently encrypted by | |
113 | * the current {@link CryptUtils}. | |
114 | * | |
12784931 | 115 | * @param out |
d20c8d77 | 116 | * the {@link OutputStream} to wrap |
12784931 | 117 | * |
d20c8d77 NR |
118 | * @return the auto-encode {@link OutputStream} |
119 | */ | |
12784931 | 120 | public OutputStream encrypt(OutputStream out) { |
0747c3c2 | 121 | Cipher ecipher = newCipher(Cipher.ENCRYPT_MODE); |
d20c8d77 NR |
122 | return new CipherOutputStream(out, ecipher); |
123 | } | |
124 | ||
125 | /** | |
12784931 NR |
126 | * Wrap the given {@link OutputStream} so it is transparently encrypted by |
127 | * the current {@link CryptUtils} and encoded in base64. | |
128 | * | |
129 | * @param out | |
130 | * the {@link OutputStream} to wrap | |
12784931 NR |
131 | * |
132 | * @return the auto-encode {@link OutputStream} | |
133 | * | |
134 | * @throws IOException | |
135 | * in case of I/O error | |
136 | */ | |
a6a73de3 NR |
137 | public OutputStream encrypt64(OutputStream out) throws IOException { |
138 | return encrypt(new Base64OutputStream(out, true)); | |
12784931 NR |
139 | } |
140 | ||
141 | /** | |
142 | * Wrap the given {@link OutputStream} so it is transparently decoded by the | |
d20c8d77 NR |
143 | * current {@link CryptUtils}. |
144 | * | |
145 | * @param in | |
146 | * the {@link InputStream} to wrap | |
12784931 | 147 | * |
d20c8d77 NR |
148 | * @return the auto-decode {@link InputStream} |
149 | */ | |
12784931 | 150 | public InputStream decrypt(InputStream in) { |
0747c3c2 | 151 | Cipher dcipher = newCipher(Cipher.DECRYPT_MODE); |
d20c8d77 NR |
152 | return new CipherInputStream(in, dcipher); |
153 | } | |
154 | ||
155 | /** | |
12784931 NR |
156 | * Wrap the given {@link OutputStream} so it is transparently decoded by the |
157 | * current {@link CryptUtils} and decoded from base64. | |
158 | * | |
159 | * @param in | |
160 | * the {@link InputStream} to wrap | |
12784931 NR |
161 | * |
162 | * @return the auto-decode {@link InputStream} | |
163 | * | |
164 | * @throws IOException | |
165 | * in case of I/O error | |
166 | */ | |
a6a73de3 NR |
167 | public InputStream decrypt64(InputStream in) throws IOException { |
168 | return decrypt(new Base64InputStream(in, false)); | |
12784931 NR |
169 | } |
170 | ||
171 | /** | |
172 | * Wrap the given {@link OutputStream} so it is transparently decoded by the | |
d20c8d77 NR |
173 | * current {@link CryptUtils}. |
174 | * | |
175 | * @param out | |
176 | * the {@link OutputStream} to wrap | |
177 | * @return the auto-decode {@link OutputStream} | |
178 | */ | |
12784931 | 179 | public OutputStream decrypt(OutputStream out) { |
0747c3c2 | 180 | Cipher dcipher = newCipher(Cipher.DECRYPT_MODE); |
d20c8d77 NR |
181 | return new CipherOutputStream(out, dcipher); |
182 | } | |
183 | ||
12784931 NR |
184 | /** |
185 | * Wrap the given {@link OutputStream} so it is transparently decoded by the | |
186 | * current {@link CryptUtils} and decoded from base64. | |
187 | * | |
188 | * @param out | |
189 | * the {@link OutputStream} to wrap | |
12784931 NR |
190 | * |
191 | * @return the auto-decode {@link OutputStream} | |
192 | * | |
193 | * @throws IOException | |
194 | * in case of I/O error | |
195 | */ | |
a6a73de3 NR |
196 | public OutputStream decrypt64(OutputStream out) throws IOException { |
197 | return new Base64OutputStream(decrypt(out), false); | |
12784931 NR |
198 | } |
199 | ||
52e0732e NR |
200 | /** |
201 | * This method required an array of 128 bytes. | |
202 | * | |
203 | * @param bytes32 | |
204 | * the array, which <b>must</b> be of 128 bits (32 bytes) | |
205 | * | |
206 | * @throws InvalidKeyException | |
207 | * if the key is not an array of 128 bits (32 bytes) | |
208 | */ | |
209 | private void init(byte[] bytes32) throws InvalidKeyException { | |
210 | if (bytes32 == null || bytes32.length != 32) { | |
211 | throw new InvalidKeyException( | |
212 | "The size of the key must be of 128 bits (32 bytes), it is: " | |
213 | + (bytes32 == null ? "null" : "" + bytes32.length) | |
214 | + " bytes"); | |
215 | } | |
216 | ||
0747c3c2 NR |
217 | key = new SecretKeySpec(bytes32, "AES"); |
218 | ecipher = newCipher(Cipher.ENCRYPT_MODE); | |
219 | dcipher = newCipher(Cipher.DECRYPT_MODE); | |
220 | } | |
12784931 | 221 | |
0747c3c2 NR |
222 | /** |
223 | * Create a new {@link Cipher}of the given mode (see | |
224 | * {@link Cipher#ENCRYPT_MODE} and {@link Cipher#ENCRYPT_MODE}). | |
225 | * | |
226 | * @param mode | |
227 | * the mode ({@link Cipher#ENCRYPT_MODE} or | |
228 | * {@link Cipher#ENCRYPT_MODE}) | |
229 | * | |
230 | * @return the new {@link Cipher} | |
231 | */ | |
232 | private Cipher newCipher(int mode) { | |
52e0732e | 233 | try { |
12784931 NR |
234 | byte[] iv = { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }; |
235 | IvParameterSpec ivspec = new IvParameterSpec(iv); | |
0747c3c2 | 236 | Cipher cipher = Cipher.getInstance(AES_NAME); |
12784931 | 237 | cipher.init(mode, key, ivspec); |
0747c3c2 | 238 | return cipher; |
52e0732e NR |
239 | } catch (NoSuchAlgorithmException e) { |
240 | // Every implementation of the Java platform is required to support | |
241 | // this standard Cipher transformation with 128 bits keys | |
242 | e.printStackTrace(); | |
243 | } catch (NoSuchPaddingException e) { | |
244 | // Every implementation of the Java platform is required to support | |
245 | // this standard Cipher transformation with 128 bits keys | |
246 | e.printStackTrace(); | |
247 | } catch (InvalidKeyException e) { | |
248 | // Every implementation of the Java platform is required to support | |
249 | // this standard Cipher transformation with 128 bits keys | |
250 | e.printStackTrace(); | |
12784931 NR |
251 | } catch (InvalidAlgorithmParameterException e) { |
252 | // Woops? | |
253 | e.printStackTrace(); | |
52e0732e | 254 | } |
12784931 | 255 | |
0747c3c2 | 256 | return null; |
52e0732e NR |
257 | } |
258 | ||
259 | /** | |
260 | * Encrypt the data. | |
261 | * | |
262 | * @param data | |
263 | * the data to encrypt | |
264 | * | |
265 | * @return the encrypted data | |
266 | * | |
9fb03c36 | 267 | * @throws SSLException |
52e0732e NR |
268 | * in case of I/O error (i.e., the data is not what you assumed |
269 | * it was) | |
270 | */ | |
9fb03c36 | 271 | public byte[] encrypt(byte[] data) throws SSLException { |
0747c3c2 NR |
272 | synchronized (ecipher) { |
273 | try { | |
274 | return ecipher.doFinal(data); | |
275 | } catch (IllegalBlockSizeException e) { | |
276 | throw new SSLException(e); | |
277 | } catch (BadPaddingException e) { | |
278 | throw new SSLException(e); | |
279 | } | |
52e0732e NR |
280 | } |
281 | } | |
282 | ||
283 | /** | |
284 | * Encrypt the data. | |
285 | * | |
286 | * @param data | |
287 | * the data to encrypt | |
288 | * | |
289 | * @return the encrypted data | |
290 | * | |
9fb03c36 | 291 | * @throws SSLException |
52e0732e NR |
292 | * in case of I/O error (i.e., the data is not what you assumed |
293 | * it was) | |
294 | */ | |
9fb03c36 NR |
295 | public byte[] encrypt(String data) throws SSLException { |
296 | try { | |
297 | return encrypt(data.getBytes("UTF8")); | |
298 | } catch (UnsupportedEncodingException e) { | |
299 | // UTF-8 is required in all confirm JVMs | |
300 | e.printStackTrace(); | |
301 | return null; | |
302 | } | |
52e0732e NR |
303 | } |
304 | ||
305 | /** | |
306 | * Encrypt the data, then encode it into Base64. | |
307 | * | |
308 | * @param data | |
309 | * the data to encrypt | |
310 | * @param zip | |
311 | * TRUE to also compress the data in GZIP format; remember that | |
312 | * compressed and not-compressed content are different; you need | |
313 | * to know which is which when decoding | |
314 | * | |
315 | * @return the encrypted data, encoded in Base64 | |
316 | * | |
9fb03c36 | 317 | * @throws SSLException |
52e0732e NR |
318 | * in case of I/O error (i.e., the data is not what you assumed |
319 | * it was) | |
320 | */ | |
a6a73de3 | 321 | public String encrypt64(String data) throws SSLException { |
9fb03c36 | 322 | try { |
a6a73de3 | 323 | return encrypt64(data.getBytes("UTF8")); |
9fb03c36 NR |
324 | } catch (UnsupportedEncodingException e) { |
325 | // UTF-8 is required in all confirm JVMs | |
326 | e.printStackTrace(); | |
327 | return null; | |
328 | } | |
52e0732e NR |
329 | } |
330 | ||
331 | /** | |
332 | * Encrypt the data, then encode it into Base64. | |
333 | * | |
334 | * @param data | |
335 | * the data to encrypt | |
52e0732e NR |
336 | * |
337 | * @return the encrypted data, encoded in Base64 | |
338 | * | |
9fb03c36 | 339 | * @throws SSLException |
52e0732e NR |
340 | * in case of I/O error (i.e., the data is not what you assumed |
341 | * it was) | |
342 | */ | |
a6a73de3 | 343 | public String encrypt64(byte[] data) throws SSLException { |
9fb03c36 | 344 | try { |
a6a73de3 | 345 | return StringUtils.base64(encrypt(data)); |
9fb03c36 NR |
346 | } catch (IOException e) { |
347 | // not exactly true, but we consider here that this error is a crypt | |
348 | // error, not a normal I/O error | |
349 | throw new SSLException(e); | |
350 | } | |
52e0732e NR |
351 | } |
352 | ||
353 | /** | |
354 | * Decode the data which is assumed to be encrypted with the same utilities. | |
355 | * | |
356 | * @param data | |
357 | * the encrypted data to decode | |
358 | * | |
359 | * @return the original, decoded data | |
360 | * | |
9fb03c36 | 361 | * @throws SSLException |
52e0732e NR |
362 | * in case of I/O error |
363 | */ | |
9fb03c36 | 364 | public byte[] decrypt(byte[] data) throws SSLException { |
0747c3c2 NR |
365 | synchronized (dcipher) { |
366 | try { | |
367 | return dcipher.doFinal(data); | |
368 | } catch (IllegalBlockSizeException e) { | |
369 | throw new SSLException(e); | |
370 | } catch (BadPaddingException e) { | |
371 | throw new SSLException(e); | |
372 | } | |
52e0732e NR |
373 | } |
374 | } | |
375 | ||
376 | /** | |
377 | * Decode the data which is assumed to be encrypted with the same utilities | |
ed3aa598 NR |
378 | * and to be a {@link String}. |
379 | * | |
380 | * @param data | |
381 | * the encrypted data to decode | |
382 | * | |
383 | * @return the original, decoded data,as a {@link String} | |
384 | * | |
385 | * @throws SSLException | |
386 | * in case of I/O error | |
387 | */ | |
388 | public String decrypts(byte[] data) throws SSLException { | |
389 | try { | |
390 | return new String(decrypt(data), "UTF-8"); | |
391 | } catch (UnsupportedEncodingException e) { | |
392 | // UTF-8 is required in all confirm JVMs | |
393 | e.printStackTrace(); | |
394 | return null; | |
395 | } | |
396 | } | |
397 | ||
398 | /** | |
399 | * Decode the data which is assumed to be encrypted with the same utilities | |
52e0732e NR |
400 | * and is a Base64 encoded value. |
401 | * | |
402 | * @param data | |
403 | * the encrypted data to decode in Base64 format | |
404 | * @param zip | |
405 | * TRUE to also uncompress the data from a GZIP format | |
406 | * automatically; if set to FALSE, zipped data can be returned | |
407 | * | |
408 | * @return the original, decoded data | |
409 | * | |
9fb03c36 | 410 | * @throws SSLException |
52e0732e NR |
411 | * in case of I/O error |
412 | */ | |
a6a73de3 | 413 | public byte[] decrypt64(String data) throws SSLException { |
9fb03c36 | 414 | try { |
a6a73de3 | 415 | return decrypt(StringUtils.unbase64(data)); |
9fb03c36 NR |
416 | } catch (IOException e) { |
417 | // not exactly true, but we consider here that this error is a crypt | |
418 | // error, not a normal I/O error | |
419 | throw new SSLException(e); | |
420 | } | |
52e0732e NR |
421 | } |
422 | ||
423 | /** | |
424 | * Decode the data which is assumed to be encrypted with the same utilities | |
425 | * and is a Base64 encoded value, then convert it into a String (this method | |
426 | * assumes the data <b>was</b> indeed a UTF-8 encoded {@link String}). | |
427 | * | |
428 | * @param data | |
429 | * the encrypted data to decode in Base64 format | |
430 | * @param zip | |
431 | * TRUE to also uncompress the data from a GZIP format | |
432 | * automatically; if set to FALSE, zipped data can be returned | |
433 | * | |
434 | * @return the original, decoded data | |
435 | * | |
9fb03c36 | 436 | * @throws SSLException |
52e0732e NR |
437 | * in case of I/O error |
438 | */ | |
a6a73de3 | 439 | public String decrypt64s(String data) throws SSLException { |
9fb03c36 | 440 | try { |
a6a73de3 | 441 | return new String(decrypt(StringUtils.unbase64(data)), "UTF-8"); |
9fb03c36 NR |
442 | } catch (UnsupportedEncodingException e) { |
443 | // UTF-8 is required in all confirm JVMs | |
444 | e.printStackTrace(); | |
445 | return null; | |
446 | } catch (IOException e) { | |
447 | // not exactly true, but we consider here that this error is a crypt | |
448 | // error, not a normal I/O error | |
449 | throw new SSLException(e); | |
450 | } | |
52e0732e NR |
451 | } |
452 | ||
453 | /** | |
454 | * This is probably <b>NOT</b> secure! | |
455 | * | |
456 | * @param input | |
457 | * some {@link String} input | |
458 | * | |
459 | * @return a 128 bits key computed from the given input | |
460 | */ | |
461 | static private byte[] key2key(String input) { | |
b1ed544b | 462 | return StringUtils.getMd5Hash("" + input).getBytes(); |
52e0732e NR |
463 | } |
464 | } |