Source code: renderkits/util/ByteArrayGuard.java
1 /*
2 * $Id: ByteArrayGuard.java,v 1.3 2005/12/14 22:27:44 rlubke Exp $
3 */
4
5 /*
6 * The contents of this file are subject to the terms
7 * of the Common Development and Distribution License
8 * (the License). You may not use this file except in
9 * compliance with the License.
10 *
11 * You can obtain a copy of the License at
12 * https://javaserverfaces.dev.java.net/CDDL.html or
13 * legal/CDDLv1.0.txt.
14 * See the License for the specific language governing
15 * permission and limitations under the License.
16 *
17 * When distributing Covered Code, include this CDDL
18 * Header Notice in each file and include the License file
19 * at legal/CDDLv1.0.txt.
20 * If applicable, add the following below the CDDL Header,
21 * with the fields enclosed by brackets [] replaced by
22 * your own identifying information:
23 * "Portions Copyrighted [year] [name of copyright owner]"
24 *
25 * [Name of File] [ver.__] [Date]
26 *
27 * Copyright 2005 Sun Microsystems Inc. All Rights Reserved
28 */
29
30 package renderkits.util;
31
32 import javax.crypto.Cipher;
33 import javax.crypto.Mac;
34 import javax.crypto.SecretKeyFactory;
35 import javax.crypto.spec.DESedeKeySpec;
36 import javax.crypto.spec.IvParameterSpec;
37 import javax.crypto.spec.SecretKeySpec;
38 import javax.faces.context.FacesContext;
39
40 import java.security.Key;
41 import java.security.MessageDigest;
42 import java.security.SecureRandom;
43 import java.util.Arrays;
44 import java.util.Map;
45 import java.util.logging.Level;
46 import java.util.logging.Logger;
47
48 /**
49 * This utility class provides services to encrypt or decrypt a byte array.
50 * The algorithm used to encrypt byte array is 3DES with CBC
51 * The algorithm used to create the message authentication code (MAC) is SHA1
52 * <p/>
53 * Original author Inderjeet Singh, J2EE Blue Prints Team. Modified to suit JSF
54 * needs.
55 */
56 public class ByteArrayGuard {
57 public static final int DEFAULT_KEY_LENGTH = 24;
58 public static final int DEFAULT_MAC_LENGTH = 20;
59 public static final int DEFAULT_IV_LENGTH = 8;
60
61 public static final String SESSION_KEY_FOR_PASSWORD =
62 "com.sun.faces.clientside-state.password-key";
63 public static final int DEFAULT_PASSWORD_LENGTH = 24;
64
65 // Log instance for this class
66 private static Logger logger;
67
68 static {
69 logger = Util.getLogger(Util.FACES_LOGGER);
70 }
71
72 /**
73 * @param ps the password strategy to create password for encryption and decryption
74 * uses default values for the length of the encryption key, MAC key, and
75 * the initialization vector
76 *
77 * @see DEFAULT_KEY_LENGTH
78 * @see DEFAULT_MAC_LENGTH
79 * @see DEFAULT_IV_LENGTH
80 */
81 public ByteArrayGuard() {
82 this(DEFAULT_KEY_LENGTH, DEFAULT_MAC_LENGTH, DEFAULT_IV_LENGTH);
83 }
84
85 /**
86 * @param keyLength the length of the key used for encryption
87 * @param macLength the length of the message authentication used
88 * @param ivLength length of the initialization vector used by the block cipher
89 * @param
90 */
91 public ByteArrayGuard(int keyLength, int macLength, int ivLength) {
92 this.keyLength = keyLength;
93 this.macLength = macLength;
94 this.ivLength = ivLength;
95 }
96
97 /**
98 * Encrypts the specified plaindata using the specified password. It also
99 * stores the MAC and the IV in the output. The 20-byte MAC is stored
100 * first, followed by the 8-byte IV, followed by the encrypted
101 * contents of the file.
102 *
103 * @param context FacesContext for this request
104 * @param plaindata The plain text that needs to be encrypted
105 *
106 * @return The encrypted contents
107 */
108 public byte[] encrypt(FacesContext context, byte[] plaindata) {
109 try {
110 // generate a key that can be used for encryption from the
111 // supplied password
112 byte[] rawKey =
113 convertPasswordToKey(getPasswordToSecureState(context));
114 // choose block encryption algorithm
115 Cipher cipher = getBlockCipherForEncryption(rawKey);
116 // encrypt the plaintext
117 byte[] encdata = cipher.doFinal(plaindata);
118 // choose mac algorithm
119 Mac mac = getMac(rawKey);
120 // generate MAC for the initialization vector of the cipher
121 byte[] iv = cipher.getIV();
122 mac.update(iv);
123 // generate MAC for the encrypted data
124 mac.update(encdata);
125 // generate MAC
126 byte[] macBytes = mac.doFinal();
127
128 // concat byte arrays for MAC, IV, and encrypted data
129 // Note that the order is important here. MAC and IV are
130 // of fixed length and need to appear before the encrypted data
131 // for easy extraction while decrypting.
132 byte[] tmp = concatBytes(macBytes, iv);
133 byte[] securedata = concatBytes(tmp, encdata);
134 return securedata;
135 } catch (Exception e) {
136 if (logger.isLoggable(Level.SEVERE)) {
137 logger.log(Level.SEVERE, e.getMessage(), e.getCause());
138 }
139 throw new RuntimeException(e);
140 }
141 }
142
143 /**
144 * Decrypts the specified byte array using the specified password, and
145 * generates an inputstream from it. The file must be encrypted by the
146 * above method for encryption. The method also verifies the MAC. It
147 * uses the IV present in the file for decryption.
148 *
149 * @param context Faces Context for this request
150 * @param securedata The encrypted data (including mac and initialization
151 * vector) that needs to be decrypted
152 *
153 * @return A byte array containing the decrypted contents
154 */
155 public byte[] decrypt(FacesContext context, byte[] securedata) {
156 try {
157 // Extract MAC
158 byte[] macBytes = new byte[macLength];
159 System.arraycopy(securedata, 0, macBytes, 0, macBytes.length);
160 // Extract initialization vector used for encryption
161 byte[] iv = new byte[ivLength];
162 System.arraycopy(securedata, macBytes.length, iv, 0, iv.length);
163
164 // Extract encrypted data
165 byte[] encdata =
166 new byte[securedata.length - macBytes.length - iv.length];
167 System.arraycopy(securedata,
168 macBytes.length + iv.length,
169 encdata,
170 0,
171 encdata.length);
172
173 // verify MAC by regenerating it and comparing it with the received value
174 byte[] rawKey =
175 convertPasswordToKey(getPasswordToSecureState(context));
176 Mac mac = getMac(rawKey);
177 mac.update(iv);
178 mac.update(encdata);
179 byte[] macBytesCalculated = mac.doFinal();
180 if (Arrays.equals(macBytes, macBytesCalculated)) {
181 // decrypt data only if the MAC was valid
182 Cipher cipher = getBlockCipherForDecryption(rawKey, iv);
183 byte[] plaindata = cipher.doFinal(encdata);
184 return plaindata;
185 } else {
186 if (logger.isLoggable(Level.WARNING)) {
187 // PENDING (visvan) localize
188 logger.warning("ERROR: MAC did not verify!");
189 }
190 return null;
191 }
192 } catch (Exception e) {
193 if (logger.isLoggable(Level.SEVERE)) {
194 logger.log(Level.SEVERE, e.getMessage(), e.getCause());
195 }
196 throw new RuntimeException(e);
197 }
198 }
199
200 /**
201 * This method provides a password to be used for encryption/decryption of
202 * client-side state.
203 */
204 private String getPasswordToSecureState(FacesContext context) {
205 Map sessionMap = context.getExternalContext().getSessionMap();
206 if (sessionMap == null) {
207 // Setting it to an arbitrary value. As long as the same value is used
208 // by both serializer and deserializer, the encryption will work.
209 // However, the encryption will be useless since this arbitrary
210 // value can be guessed by an attacker.
211 // PENDING (visvan) localize
212 if (logger.isLoggable(Level.WARNING)) {
213 logger.warning(
214 "Key to retrieve password from session could not " +
215 "be found. Using default value. This will enable " +
216 "the encryption and decryption to work, but the " +
217 "client-side state saving method is NO longer secure.");
218 }
219 password = "easy-to-guess-password";
220 } else {
221 password = (String) sessionMap.get(SESSION_KEY_FOR_PASSWORD);
222 if (password == null) {
223 password = (String)
224 ByteArrayGuard.getRandomString(DEFAULT_PASSWORD_LENGTH);
225 sessionMap.put(SESSION_KEY_FOR_PASSWORD, password);
226 }
227 }
228 return password;
229 }
230
231 /**
232 * This method converts the specified password into a key in a
233 * deterministic manner. The key is then usable for creating ciphers
234 * and MACs.
235 *
236 * @return a byte array containing a key based on the specified
237 * password. The length of the returned byte array is KEY_LENGTH.
238 */
239 private byte[] convertPasswordToKey(byte[] password) {
240 try {
241 MessageDigest md = MessageDigest.getInstance("SHA");
242 byte[] seed = md.digest(password);
243
244 SecureRandom random = SecureRandom.getInstance("SHA1PRNG");
245 random.setSeed(seed);
246
247 byte[] rawkey = new byte[keyLength];
248 random.nextBytes(rawkey);
249 return rawkey;
250 } catch (Exception e) {
251 throw new RuntimeException(e);
252 }
253 }
254
255 /**
256 * A convenience alias to the above method which takes a string as
257 * the password.
258 */
259 private byte[] convertPasswordToKey(String password) {
260 return convertPasswordToKey(password.getBytes());
261 }
262
263 /**
264 * @param rawKey must be 24 bytes in length.
265 *
266 * @return a 3DES block cipher to be used for encryption based on the
267 * specified key
268 */
269 private Cipher getBlockCipherForEncryption(byte[] rawKey) {
270 try {
271 SecretKeyFactory keygen = SecretKeyFactory.getInstance("DESede");
272 DESedeKeySpec keyspec = new DESedeKeySpec(rawKey);
273 Key key = keygen.generateSecret(keyspec);
274 Cipher cipher = Cipher.getInstance("DESede/CBC/PKCS5Padding");
275 byte[] iv = new byte[ivLength];
276 getPRNG().nextBytes(iv);
277 IvParameterSpec ivspec = new IvParameterSpec(iv);
278 cipher.init(Cipher.ENCRYPT_MODE, key, ivspec, getPRNG());
279 return cipher;
280 } catch (Exception e) {
281 throw new RuntimeException(e);
282 }
283 }
284
285 private static Cipher getBlockCipherForDecryption(byte[] rawKey, byte[]
286 iv) {
287 try {
288 SecretKeyFactory keygen = SecretKeyFactory.getInstance("DESede");
289 DESedeKeySpec keyspec = new DESedeKeySpec(rawKey);
290 Key key = keygen.generateSecret(keyspec);
291 Cipher cipher = Cipher.getInstance("DESede/CBC/PKCS5Padding");
292 IvParameterSpec ivspec = new IvParameterSpec(iv);
293 cipher.init(Cipher.DECRYPT_MODE, key, ivspec, getPRNG());
294 return cipher;
295 } catch (Exception e) {
296 throw new RuntimeException(e);
297 }
298 }
299
300 private Mac getMac(byte[] rawKey) {
301 try {
302 Mac mac = Mac.getInstance("HmacSHA1");
303 SecretKeySpec key =
304 new SecretKeySpec(rawKey, 0, macLength, "HmacSHA1");
305 mac.init(key);
306 return mac;
307 } catch (Exception e) {
308 throw new RuntimeException(e);
309 }
310 }
311
312 /**
313 * Generates a cryptographically random string
314 *
315 * @param size the desired length of the string
316 */
317 static String getRandomString(int size) {
318 byte[] data = new byte[size];
319 getPRNG().nextBytes(data);
320 return new String(data);
321 }
322
323 private static int getRandomInt() {
324 byte[] data = new byte[4];
325 getPRNG().nextBytes(data);
326 return data[0] + data[1] * 256 + data[2] * 65536 + data[3] * 16777216;
327 }
328
329 private static SecureRandom getPRNG() {
330 try {
331 if (prng == null) {
332 prng = SecureRandom.getInstance("SHA1PRNG");
333 }
334 return prng;
335 } catch (Exception e) {
336 throw new RuntimeException(e);
337 }
338 }
339
340 private static String getHexString(byte[] b) {
341 StringBuffer buf = new StringBuffer(b.length);
342 for (int i = 0; i < b.length; ++i) {
343 byte2hex(b[i], buf);
344 }
345 return buf.toString();
346 }
347
348 /**
349 * This method concatenates two byte arrays
350 *
351 * @param array1 first byte array to be concatenated
352 * @param array2 second byte array to be concatenated
353 *
354 * @return a byte array of array1||array2
355 */
356 private static byte[] concatBytes(byte[] array1, byte[] array2) {
357 byte[] cBytes = new byte[array1.length + array2.length];
358 try {
359 System.arraycopy(array1, 0, cBytes, 0, array1.length);
360 System.arraycopy(array2, 0, cBytes, array1.length, array2.length);
361 } catch (Exception e) {
362 throw new RuntimeException(e);
363 }
364 return cBytes;
365 }
366
367 /** Converts a byte to hex digit and writes to the supplied buffer */
368 private static void byte2hex(byte b, StringBuffer buf) {
369 char[] hexChars = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
370 'A', 'B', 'C', 'D', 'E', 'F'};
371 int high = ((b & 0xf0) >> 4);
372 int low = (b & 0x0f);
373 buf.append(hexChars[high]);
374 buf.append(hexChars[low]);
375 }
376
377 private int keyLength;
378 private int macLength;
379 private int ivLength;
380 private String password = null;
381 private static SecureRandom prng = null;
382 }