Bundle: fix memory leak at init/reset
[fanfix.git] / CryptUtils.java
1 package be.nikiroo.utils;
2
3 import java.io.IOException;
4 import java.io.InputStream;
5 import java.io.OutputStream;
6 import java.io.UnsupportedEncodingException;
7 import java.security.InvalidKeyException;
8
9 import javax.crypto.BadPaddingException;
10 import javax.crypto.Cipher;
11 import javax.crypto.CipherInputStream;
12 import javax.crypto.CipherOutputStream;
13 import javax.crypto.IllegalBlockSizeException;
14 import javax.crypto.spec.IvParameterSpec;
15 import javax.crypto.spec.SecretKeySpec;
16 import javax.net.ssl.SSLException;
17
18 import be.nikiroo.utils.streams.Base64InputStream;
19 import be.nikiroo.utils.streams.Base64OutputStream;
20
21 /**
22 * Small utility class to do AES encryption/decryption.
23 * <p>
24 * It is multi-thread compatible, but beware:
25 * <ul>
26 * <li>The encrypt/decrypt calls are serialized</li>
27 * <li>The streams are independent (and thus parallel)</li>
28 * </ul>
29 * <p>
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.
33 *
34 * @author niki
35 */
36 public class CryptUtils {
37 static private final String AES_NAME = "AES/CFB128/NoPadding";
38
39 private Cipher ecipher;
40 private Cipher dcipher;
41 private byte[] bytes32;
42
43 /**
44 * Small and lazy-easy way to initialize a 128 bits key with
45 * {@link CryptUtils}.
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 {
58 init(key2key(key));
59 } catch (InvalidKeyException e) {
60 // We made sure that the key is correct, so nothing here
61 e.printStackTrace();
62 }
63 }
64
65 /**
66 * Create a new instance of {@link CryptUtils} with the given 128 bits key.
67 * <p>
68 * The key <b>must</b> be exactly 128 bits long.
69 *
70 * @param bytes32
71 * the 128 bits (32 bytes) of the key
72 *
73 * @throws InvalidKeyException
74 * if the key is not an array of 128 bits
75 */
76 public CryptUtils(byte[] bytes32) throws InvalidKeyException {
77 init(bytes32);
78 }
79
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 */
88 public InputStream encrypt(InputStream in) {
89 Cipher ecipher = newCipher(Cipher.ENCRYPT_MODE);
90 return new CipherInputStream(in, ecipher);
91 }
92
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
99 *
100 * @return the auto-encode {@link InputStream}
101 *
102 * @throws IOException
103 * in case of I/O error
104 */
105 public InputStream encrypt64(InputStream in) throws IOException {
106 return new Base64InputStream(encrypt(in), true);
107 }
108
109 /**
110 * Wrap the given {@link OutputStream} so it is transparently encrypted by
111 * the current {@link CryptUtils}.
112 *
113 * @param out
114 * the {@link OutputStream} to wrap
115 *
116 * @return the auto-encode {@link OutputStream}
117 */
118 public OutputStream encrypt(OutputStream out) {
119 Cipher ecipher = newCipher(Cipher.ENCRYPT_MODE);
120 return new CipherOutputStream(out, ecipher);
121 }
122
123 /**
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
129 *
130 * @return the auto-encode {@link OutputStream}
131 *
132 * @throws IOException
133 * in case of I/O error
134 */
135 public OutputStream encrypt64(OutputStream out) throws IOException {
136 return encrypt(new Base64OutputStream(out, true));
137 }
138
139 /**
140 * Wrap the given {@link OutputStream} so it is transparently decoded by the
141 * current {@link CryptUtils}.
142 *
143 * @param in
144 * the {@link InputStream} to wrap
145 *
146 * @return the auto-decode {@link InputStream}
147 */
148 public InputStream decrypt(InputStream in) {
149 Cipher dcipher = newCipher(Cipher.DECRYPT_MODE);
150 return new CipherInputStream(in, dcipher);
151 }
152
153 /**
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
159 *
160 * @return the auto-decode {@link InputStream}
161 *
162 * @throws IOException
163 * in case of I/O error
164 */
165 public InputStream decrypt64(InputStream in) throws IOException {
166 return decrypt(new Base64InputStream(in, false));
167 }
168
169 /**
170 * Wrap the given {@link OutputStream} so it is transparently decoded by the
171 * current {@link CryptUtils}.
172 *
173 * @param out
174 * the {@link OutputStream} to wrap
175 * @return the auto-decode {@link OutputStream}
176 */
177 public OutputStream decrypt(OutputStream out) {
178 Cipher dcipher = newCipher(Cipher.DECRYPT_MODE);
179 return new CipherOutputStream(out, dcipher);
180 }
181
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
188 *
189 * @return the auto-decode {@link OutputStream}
190 *
191 * @throws IOException
192 * in case of I/O error
193 */
194 public OutputStream decrypt64(OutputStream out) throws IOException {
195 return new Base64OutputStream(decrypt(out), false);
196 }
197
198 /**
199 * This method required an array of 128 bits.
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
215 this.bytes32 = bytes32;
216 this.ecipher = newCipher(Cipher.ENCRYPT_MODE);
217 this.dcipher = newCipher(Cipher.DECRYPT_MODE);
218 }
219
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) {
231 try {
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 }
237 IvParameterSpec ivspec = new IvParameterSpec(iv);
238 Cipher cipher = Cipher.getInstance(AES_NAME);
239 cipher.init(mode, new SecretKeySpec(bytes32, "AES"), ivspec);
240 return cipher;
241 } catch (Exception e) {
242 e.printStackTrace();
243 throw new RuntimeException(
244 "Cannot initialize encryption sub-system", e);
245 }
246 }
247
248 /**
249 * Encrypt the data.
250 *
251 * @param data
252 * the data to encrypt
253 *
254 * @return the encrypted data
255 *
256 * @throws SSLException
257 * in case of I/O error (i.e., the data is not what you assumed
258 * it was)
259 */
260 public byte[] encrypt(byte[] data) throws SSLException {
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 }
269 }
270 }
271
272 /**
273 * Encrypt the data.
274 *
275 * @param data
276 * the data to encrypt
277 *
278 * @return the encrypted data
279 *
280 * @throws SSLException
281 * in case of I/O error (i.e., the data is not what you assumed
282 * it was)
283 */
284 public byte[] encrypt(String data) throws SSLException {
285 return encrypt(StringUtils.getBytes(data));
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 *
300 * @throws SSLException
301 * in case of I/O error (i.e., the data is not what you assumed
302 * it was)
303 */
304 public String encrypt64(String data) throws SSLException {
305 return encrypt64(StringUtils.getBytes(data));
306 }
307
308 /**
309 * Encrypt the data, then encode it into Base64.
310 *
311 * @param data
312 * the data to encrypt
313 *
314 * @return the encrypted data, encoded in Base64
315 *
316 * @throws SSLException
317 * in case of I/O error (i.e., the data is not what you assumed
318 * it was)
319 */
320 public String encrypt64(byte[] data) throws SSLException {
321 try {
322 return StringUtils.base64(encrypt(data));
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 }
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 *
338 * @throws SSLException
339 * in case of I/O error
340 */
341 public byte[] decrypt(byte[] data) throws SSLException {
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 }
350 }
351 }
352
353 /**
354 * Decode the data which is assumed to be encrypted with the same utilities
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) {
369 // UTF-8 is required in all conform JVMs
370 e.printStackTrace();
371 return null;
372 }
373 }
374
375 /**
376 * Decode the data which is assumed to be encrypted with the same utilities
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 *
387 * @throws SSLException
388 * in case of I/O error
389 */
390 public byte[] decrypt64(String data) throws SSLException {
391 try {
392 return decrypt(StringUtils.unbase64(data));
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 }
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 *
413 * @throws SSLException
414 * in case of I/O error
415 */
416 public String decrypt64s(String data) throws SSLException {
417 try {
418 return new String(decrypt(StringUtils.unbase64(data)), "UTF-8");
419 } catch (UnsupportedEncodingException e) {
420 // UTF-8 is required in all conform JVMs
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 }
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) {
439 return StringUtils.getMd5Hash("" + input).getBytes();
440 }
441 }