Started to sketch cache functionality.
[java-idp.git] / src / edu / internet2 / middleware / shibboleth / common / provider / CookieCache.java
1 /*
2  * Copyright [2005] [University Corporation for Advanced Internet Development, Inc.]
3  *
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
7  *
8  * http://www.apache.org/licenses/LICENSE-2.0
9  *
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.
15  */
16
17 package edu.internet2.middleware.shibboleth.common.provider;
18
19 import java.io.ByteArrayOutputStream;
20 import java.io.DataOutputStream;
21 import java.io.IOException;
22 import java.security.GeneralSecurityException;
23 import java.security.KeyException;
24 import java.security.SecureRandom;
25 import java.util.ArrayList;
26 import java.util.Date;
27 import java.util.HashMap;
28 import java.util.List;
29 import java.util.Map;
30 import java.util.zip.GZIPOutputStream;
31
32 import javax.crypto.Cipher;
33 import javax.crypto.Mac;
34 import javax.crypto.SecretKey;
35 import javax.crypto.spec.IvParameterSpec;
36 import javax.servlet.http.Cookie;
37 import javax.servlet.http.HttpServletRequest;
38 import javax.servlet.http.HttpServletResponse;
39
40 import edu.internet2.middleware.shibboleth.common.Cache;
41 import edu.internet2.middleware.shibboleth.utils.Base32;
42
43 /**
44  * <code>Cache</code> implementation that uses browser cookies to store data. Symmetric and HMAC algorithms are used
45  * to encrypt and verify the data. Due to the size limitations of cookie storage, data may interleaved among multiple
46  * cookies.
47  * 
48  * @author Walter Hoehn
49  */
50 public class CookieCache extends BaseCache implements Cache {
51
52         // TODO domain limit?
53         private HttpServletResponse response;
54         private List<Cookie> myCurrentCookies = new ArrayList<Cookie>();
55         private Map<String, CacheEntry> dataCache = new HashMap<String, CacheEntry>();
56         private static final int CHUNK_SIZE = 4 * 1024; // in KB, minimal browser requirement
57         private static final int COOKIE_LIMIT = 20; // minimal browser requirement
58         private static final String NAME_PREFIX = "IDP_CACHE:";
59         protected SecretKey secret;
60         private static SecureRandom random = new SecureRandom();
61         private String cipherAlgorithm = "DESede/CBC/PKCS5Padding";
62         private String macAlgorithm = "HmacSHA1";
63         private String storeType = "JCEKS";
64
65         CookieCache(String name, HttpServletRequest request, HttpServletResponse response) {
66
67                 super(name, Cache.CacheType.CLIENT_SIDE);
68                 this.response = response;
69                 Cookie[] requestCookies = request.getCookies();
70                 for (int i = 0; i < requestCookies.length; i++) {
71                         if (requestCookies[i].getName().startsWith(NAME_PREFIX)) {
72                                 myCurrentCookies.add(requestCookies[i]);
73                         }
74                 }
75
76                 // TODO dechunk, decrypt, and pull in dataCache
77         }
78
79         public boolean contains(String key) {
80
81                 CacheEntry entry = dataCache.get(key);
82
83                 if (entry == null) { return false; }
84
85                 // Clean cache if it is expired
86                 if (new Date().after(((CacheEntry) entry).expiration)) {
87                         deleteFromCache(key);
88                         return false;
89                 }
90
91                 // OK, we have it
92                 return true;
93         }
94
95         private void deleteFromCache(String key) {
96
97                 dataCache.remove(key);
98                 flushCache();
99         }
100
101         public Object retrieve(String key) {
102
103                 CacheEntry entry = dataCache.get(key);
104
105                 if (entry == null) { return null; }
106
107                 // Clean cache if it is expired
108                 if (new Date().after(((CacheEntry) entry).expiration)) {
109                         deleteFromCache(key);
110                         return null;
111                 }
112
113                 // OK, we have it
114                 return entry.value;
115         }
116
117         public void store(String key, String value, long duration) {
118
119                 dataCache.put(key, new CacheEntry(value, duration));
120                 flushCache();
121         }
122
123         /**
124          * Secures, encodes, and writes out (to cookies) cached data.
125          */
126         private void flushCache() {
127
128                 // TODO create String representation of all cache data
129                 String stringData = null;
130
131                 try {
132
133                         // Setup a gzipped data stream
134                         ByteArrayOutputStream byteStream = new ByteArrayOutputStream();
135                         GZIPOutputStream compressedStream = new GZIPOutputStream(byteStream);
136                         DataOutputStream dataStream = new DataOutputStream(compressedStream);
137
138                         // Write data and HMAC to stream
139                         Mac mac = Mac.getInstance(macAlgorithm);
140                         mac.init(secret);
141                         dataStream.write(mac.doFinal(stringData.getBytes()));
142                         dataStream.writeUTF(stringData);
143
144                         // Flush
145                         dataStream.flush();
146                         compressedStream.flush();
147                         compressedStream.finish();
148                         byteStream.flush();
149
150                         // Setup encryption lib
151                         Cipher cipher = Cipher.getInstance(cipherAlgorithm);
152                         byte[] iv = new byte[cipher.getBlockSize()];
153                         random.nextBytes(iv);
154                         IvParameterSpec ivSpec = new IvParameterSpec(iv);
155                         cipher.init(Cipher.ENCRYPT_MODE, secret, ivSpec);
156
157                         // Creat byte array of IV and encrypted cache
158                         byte[] encryptedData = cipher.doFinal(byteStream.toByteArray());
159                         byte[] cacheBytes = new byte[iv.length + encryptedData.length];
160                         // Write IV
161                         System.arraycopy(iv, 0, cacheBytes, 0, iv.length);
162                         // Write encrypted cache
163                         System.arraycopy(encryptedData, 0, cacheBytes, iv.length, encryptedData.length);
164
165                         // Base32 encode
166                         String encodedData = Base32.encode(cacheBytes);
167
168                         // Put into cookies
169                         interleaveInCookies(encodedData);
170
171                 } catch (KeyException e) {
172                         // TODO handle
173                 } catch (GeneralSecurityException e) {
174                         // TODO handle
175                 } catch (IOException e) {
176                         // TODO handle
177                 }
178         }
179
180         /**
181          * Writes encoded data across multiple cookies
182          */
183         private void interleaveInCookies(String data) {
184
185                 // Convert the String data to a list of cookies
186                 List<Cookie> cookiesToResponse = new ArrayList<Cookie>();
187                 StringBuffer bufferredData = new StringBuffer(data);
188                 while (bufferredData != null && bufferredData.length() > 0) {
189                         Cookie cookie = null;
190                         String name = null;
191                         if (bufferredData.length() <= getCookieSpace()) {
192                                 cookie = new Cookie(name, bufferredData.toString());
193                                 bufferredData = null;
194                         } else {
195                                 cookie = new Cookie(name, bufferredData.substring(0, getCookieSpace() - 1));
196                                 bufferredData.delete(0, getCookieSpace() - 1);
197                         }
198                         cookiesToResponse.add(cookie);
199                 }
200
201                 // We have to null out cookies that are no longer needed
202                 for (Cookie previousCookie : myCurrentCookies) {
203                         if (!cookiesToResponse.contains(previousCookie)) {
204                                 cookiesToResponse.add(new Cookie(previousCookie.getName(), null));
205                         }
206                 }
207
208                 // Write our cookies to the response object
209                 for (Cookie cookie : cookiesToResponse) {
210                         response.addCookie(cookie);
211                 }
212
213                 // Update our cached copy of the cookies
214                 myCurrentCookies = cookiesToResponse;
215         }
216
217         /**
218          * Returns the amount of value space available in cookies we create
219          */
220         private int getCookieSpace() {
221
222                 // TODO this needs to be better
223                 return 3000;
224         }
225
226 }