2 * Copyright [2005] [University Corporation for Advanced Internet Development, Inc.]
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
8 * http://www.apache.org/licenses/LICENSE-2.0
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
17 package edu.internet2.middleware.shibboleth.common.provider;
19 import java.io.ByteArrayInputStream;
20 import java.io.ByteArrayOutputStream;
21 import java.io.ObjectInputStream;
22 import java.io.ObjectOutputStream;
23 import java.io.StringReader;
24 import java.io.StringWriter;
25 import java.security.GeneralSecurityException;
26 import java.security.SecureRandom;
27 import java.util.ArrayList;
28 import java.util.Arrays;
29 import java.util.Collection;
30 import java.util.Date;
31 import java.util.HashMap;
32 import java.util.List;
34 import java.util.Map.Entry;
35 import java.util.zip.GZIPInputStream;
36 import java.util.zip.GZIPOutputStream;
38 import javax.crypto.Cipher;
39 import javax.crypto.Mac;
40 import javax.crypto.SecretKey;
41 import javax.crypto.spec.IvParameterSpec;
42 import javax.servlet.http.Cookie;
43 import javax.servlet.http.HttpServletRequest;
44 import javax.servlet.http.HttpServletResponse;
45 import javax.xml.parsers.DocumentBuilderFactory;
46 import javax.xml.transform.OutputKeys;
47 import javax.xml.transform.Transformer;
48 import javax.xml.transform.TransformerFactory;
49 import javax.xml.transform.dom.DOMSource;
50 import javax.xml.transform.stream.StreamResult;
52 import org.apache.log4j.Logger;
53 import org.w3c.dom.Document;
54 import org.w3c.dom.Element;
55 import org.w3c.dom.NodeList;
56 import org.xml.sax.InputSource;
58 import edu.internet2.middleware.shibboleth.common.Cache;
59 import edu.internet2.middleware.shibboleth.common.CacheException;
60 import edu.internet2.middleware.shibboleth.utils.Base32;
63 * <code>Cache</code> implementation that uses browser cookies to store data. Symmetric and HMAC algorithms are used
64 * to encrypt and verify the data. Due to the size limitations of cookie storage, data may interleaved among multiple
65 * cookies. NOTE: Using this cache implementation in a standalon tomcat configuration will usually require that the
66 * "maxHttpHeaderSize" parameter be greatly increased.
68 * @author Walter Hoehn
70 public class CookieCache extends BaseCache implements Cache {
72 private static Logger log = Logger.getLogger(CookieCache.class.getName());
73 private HttpServletResponse response;
74 private Collection<Cookie> myCurrentCookies = new ArrayList<Cookie>();
75 private Map<String, CacheEntry> dataCache = new HashMap<String, CacheEntry>();
76 private static final int CHUNK_SIZE = 4 * 1024; // minimal browser requirement
77 private static final int COOKIE_LIMIT = 20; // minimal browser requirement
78 private static final String NAME_PREFIX = "IDP_CACHE:";
79 private static int totalCookies = 0;
80 protected SecretKey secret;
81 private static SecureRandom random = new SecureRandom();
82 private String cipherAlgorithm;
83 private String macAlgorithm;
85 public CookieCache(String name, SecretKey key, String cipherAlgorithm, String macAlgorithm,
86 HttpServletRequest request, HttpServletResponse response) throws CacheException {
88 super(name, Cache.CacheType.CLIENT_SIDE);
90 this.cipherAlgorithm = cipherAlgorithm;
91 this.macAlgorithm = macAlgorithm;
92 this.response = response;
93 Cookie[] requestCookies = request.getCookies();
94 if (requestCookies != null) {
95 for (int i = 0; i < requestCookies.length; i++) {
96 if (requestCookies[i].getName().startsWith(NAME_PREFIX + getName())
97 && requestCookies[i].getValue() != null) {
98 myCurrentCookies.add(requestCookies[i]);
103 if (usingDefaultSecret()) {
104 log.warn("You are running the Cookie Cache with the "
105 + "default secret key. This is UNSAFE! Please change "
106 + "this configuration and restart the IdP.");
112 public void postProcessing() throws CacheException {
114 if (totalCookies > (COOKIE_LIMIT - 1)) {
115 log.warn("The Cookie Cache mechanism is about to write a large amount of data to the "
116 + "client. This may not work with some browser software, so it is recommended"
117 + " that you investigate other caching mechanisms.");
123 public boolean contains(String key) throws CacheException {
125 CacheEntry entry = dataCache.get(key);
127 if (entry == null) { return false; }
129 // Clean cache if it is expired
130 if (new Date().after(((CacheEntry) entry).expiration)) {
131 log.debug("Found expired object. Deleting...");
133 dataCache.remove(key);
141 public String retrieve(String key) throws CacheException {
143 CacheEntry entry = dataCache.get(key);
145 if (entry == null) { return null; }
147 // Clean cache if it is expired
148 if (new Date().after(((CacheEntry) entry).expiration)) {
149 log.debug("Found expired object. Deleting...");
151 dataCache.remove(key);
159 public void remove(String key) throws CacheException {
161 dataCache.remove(key);
165 public void store(String key, String value, long duration) throws CacheException {
167 dataCache.put(key, new CacheEntry(value, duration));
171 private void initFromCookies() throws CacheException {
173 log.debug("Attempting to initialize cache from client-supplied cookies.");
174 // Pull data from cookies
175 List<Cookie> relevantCookies = new ArrayList<Cookie>();
176 for (Cookie cookie : myCurrentCookies) {
177 if (cookie.getName().startsWith(NAME_PREFIX + getName())) {
178 relevantCookies.add(cookie);
181 if (relevantCookies.isEmpty()) {
182 log.debug("No applicable cookies found. Cache is empty.");
187 String[] sortedCookieValues = new String[relevantCookies.size()];
188 for (Cookie cookie : relevantCookies) {
189 String[] tokenizedName = cookie.getName().split(":");
190 sortedCookieValues[Integer.parseInt(tokenizedName[tokenizedName.length - 1]) - 1] = cookie.getValue();
193 StringBuffer concat = new StringBuffer();
194 for (String cookieValue : sortedCookieValues) {
195 concat.append(cookieValue);
197 log.debug("Dumping Encrypted/Encoded Input Cache: " + concat);
201 byte[] in = Base32.decode(concat.toString());
204 Cipher cipher = Cipher.getInstance(cipherAlgorithm);
205 int ivSize = cipher.getBlockSize();
206 byte[] iv = new byte[ivSize];
207 Mac mac = Mac.getInstance(macAlgorithm);
209 int macSize = mac.getMacLength();
210 if (in.length < ivSize) {
211 log.error("Cache is malformed (not enough bytes).");
212 throw new CacheException("Cache is malformed (not enough bytes).");
215 // extract the IV, setup the cipher and extract the encrypted data
216 System.arraycopy(in, 0, iv, 0, ivSize);
217 IvParameterSpec ivSpec = new IvParameterSpec(iv);
218 cipher.init(Cipher.DECRYPT_MODE, secret, ivSpec);
219 byte[] encryptedData = new byte[in.length - iv.length];
220 System.arraycopy(in, ivSize, encryptedData, 0, in.length - iv.length);
222 // decrypt the rest of the data andsetup the streams
223 byte[] decryptedBytes = cipher.doFinal(encryptedData);
224 ByteArrayInputStream byteStream = new ByteArrayInputStream(decryptedBytes);
225 GZIPInputStream compressedData = new GZIPInputStream(byteStream);
226 ObjectInputStream dataStream = new ObjectInputStream(compressedData);
228 // extract the components
229 byte[] decodedMac = new byte[macSize];
231 int bytesRead = dataStream.read(decodedMac);
232 if (bytesRead != macSize) {
233 log.error("Error parsing cache: Unable to extract HMAC.");
234 throw new CacheException("Error parsing cache: Unable to extract HMAC.");
237 String decodedData = (String) dataStream.readObject();
238 log.debug("Dumping Raw Input Cache: " + decodedData);
241 byte[] generatedMac = mac.doFinal(decodedData.getBytes());
242 if (!Arrays.equals(decodedMac, generatedMac)) {
243 log.error("Cookie cache data failed integrity check.");
244 throw new GeneralSecurityException("Cookie cache data failed integrity check.");
248 DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
249 factory.setValidating(false);
250 factory.setNamespaceAware(false);
251 Element cacheElement = factory.newDocumentBuilder().parse(new InputSource(new StringReader(decodedData)))
252 .getDocumentElement();
253 NodeList items = cacheElement.getElementsByTagName("Item");
254 for (int i = 0; i < items.getLength(); i++) {
255 Element item = (Element) items.item(i);
257 dataCache.put(item.getAttribute("key"), new CacheEntry(item.getAttribute("value"), new Date(new Long(
258 item.getAttribute("expire")))));
261 } catch (Exception e) {
262 log.error("Error decrypting cache data: " + e);
263 throw new CacheException("Unable to read cached data.");
267 private boolean usingDefaultSecret() {
269 byte[] defaultKey = new byte[]{(byte) 0xC7, (byte) 0x49, (byte) 0x80, (byte) 0xD3, (byte) 0x02, (byte) 0x4A,
270 (byte) 0x61, (byte) 0xEF, (byte) 0x25, (byte) 0x5D, (byte) 0xE3, (byte) 0x2F, (byte) 0x57, (byte) 0x51,
271 (byte) 0x20, (byte) 0x15, (byte) 0xC7, (byte) 0x49, (byte) 0x80, (byte) 0xD3, (byte) 0x02, (byte) 0x4A,
272 (byte) 0x61, (byte) 0xEF};
273 byte[] encodedKey = secret.getEncoded();
274 return Arrays.equals(defaultKey, encodedKey);
278 * Secures, encodes, and writes out (to cookies) cached data.
280 private void flushCache() throws CacheException {
282 log.debug("Flushing cache.");
283 log.debug("Encrypting cache data.");
285 // Create XML/String representation of all cache data
286 String stringData = null;
290 DocumentBuilderFactory docFactory = DocumentBuilderFactory.newInstance();
291 docFactory.setNamespaceAware(false);
292 Document placeHolder = docFactory.newDocumentBuilder().newDocument();
294 Element cacheNode = placeHolder.createElement("Cache");
295 for (Entry<String, CacheEntry> entry : dataCache.entrySet()) {
296 Element itemNode = placeHolder.createElement("Item");
297 itemNode.setAttribute("key", entry.getKey());
298 itemNode.setAttribute("value", entry.getValue().value);
299 itemNode.setAttribute("expire", new Long(entry.getValue().expiration.getTime()).toString());
300 cacheNode.appendChild(itemNode);
303 TransformerFactory factory = TransformerFactory.newInstance();
304 DOMSource source = new DOMSource(cacheNode);
305 Transformer transformer = factory.newTransformer();
306 transformer.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "yes");
307 StringWriter stringWriter = new StringWriter();
308 StreamResult result = new StreamResult(stringWriter);
309 transformer.transform(source, result);
310 stringData = stringWriter.toString().replaceAll(">\\s<", "><");
311 log.debug("Dumping Raw Cache: " + stringData);
313 } catch (Exception e) {
314 log.error("Error encoding cache data: " + e);
315 throw new CacheException("Unable to cache data.");
320 // Setup a gzipped data stream
321 ByteArrayOutputStream byteStream = new ByteArrayOutputStream();
322 GZIPOutputStream compressedStream = new GZIPOutputStream(byteStream);
323 ObjectOutputStream dataStream = new ObjectOutputStream(compressedStream);
325 // Write data and HMAC to stream
326 Mac mac = Mac.getInstance(macAlgorithm);
328 dataStream.write(mac.doFinal(stringData.getBytes()));
329 dataStream.writeObject(stringData);
332 // dataStream.flush();
333 compressedStream.flush();
334 compressedStream.finish();
337 // Setup encryption lib
338 Cipher cipher = Cipher.getInstance(cipherAlgorithm);
339 byte[] iv = new byte[cipher.getBlockSize()];
340 random.nextBytes(iv);
341 IvParameterSpec ivSpec = new IvParameterSpec(iv);
342 cipher.init(Cipher.ENCRYPT_MODE, secret, ivSpec);
344 // Creat byte array of IV and encrypted cache
345 byte[] encryptedData = cipher.doFinal(byteStream.toByteArray());
346 byte[] cacheBytes = new byte[iv.length + encryptedData.length];
348 System.arraycopy(iv, 0, cacheBytes, 0, iv.length);
349 // Write encrypted cache
350 System.arraycopy(encryptedData, 0, cacheBytes, iv.length, encryptedData.length);
353 String encodedData = Base32.encode(cacheBytes);
354 log.debug("Dumping Encrypted/Encoded Cache: " + encodedData);
357 interleaveInCookies(encodedData);
359 } catch (Exception e) {
360 log.error("Error encrypting cache data: " + e);
361 throw new CacheException("Unable to cache data.");
366 * Writes encoded data across multiple cookies
368 private void interleaveInCookies(String data) {
370 log.debug("Writing cache to cookies.");
372 // Convert the String data to a list of cookies
373 Map<String, Cookie> cookiesToResponse = new HashMap<String, Cookie>();
374 StringBuffer bufferredData = new StringBuffer(data);
376 while (bufferredData != null && bufferredData.length() > 0) {
377 Cookie cookie = null;
378 String name = NAME_PREFIX + getName() + ":" + i++;
379 if (bufferredData.length() <= getCookieSpace(name)) {
380 cookie = new Cookie(name, bufferredData.toString());
381 bufferredData = null;
383 cookie = new Cookie(name, bufferredData.substring(0, getCookieSpace(name) - 1));
384 bufferredData.delete(0, getCookieSpace(name) - 1);
386 cookiesToResponse.put(cookie.getName(), cookie);
389 // Expire cookies that we used previously but no longer need
390 for (Cookie currCookie : myCurrentCookies) {
391 if (!cookiesToResponse.containsKey(currCookie.getName())) {
392 currCookie.setMaxAge(0);
393 currCookie.setValue(null);
394 cookiesToResponse.put(currCookie.getName(), currCookie);
398 // Write our cookies to the response object
399 for (Cookie cookie : cookiesToResponse.values()) {
400 response.addCookie(cookie);
403 // Update our cached copy of the cookies
404 myCurrentCookies = cookiesToResponse.values();
408 * Returns the amount of value space available in cookies we create
410 private int getCookieSpace(String cookieName) {
412 // If we add other cookie variables, we would need to adjust this algorithm appropriately
413 StringBuffer used = new StringBuffer();
414 used.append("Set-Cookie: ");
415 used.append(cookieName + "=" + " ");
416 return CHUNK_SIZE - used.length() - 2;