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