View Javadoc

1   /*
2    * Copyright 2009-2010 Capgemini
3    * Licensed under the Apache License, Version 2.0 (the "License"); 
4    * you may not use this file except in compliance with the License. 
5    * You may obtain a copy of the License at 
6    * 
7    * http://www.apache.org/licenses/LICENSE-2.0 
8    * 
9    * Unless required by applicable law or agreed to in writing, software 
10   * distributed under the License is distributed on an "AS IS" BASIS, 
11   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 
12   * See the License for the specific language governing permissions and 
13   * limitations under the License. 
14   * 
15   */
16  package net.sourceforge.statelessfilter.backend.jsonaescookie;
17  
18  import java.io.IOException;
19  import java.security.SignatureException;
20  import java.util.List;
21  import java.util.Map;
22  import java.util.Set;
23  
24  import javax.crypto.Cipher;
25  import javax.crypto.spec.IvParameterSpec;
26  import javax.crypto.spec.SecretKeySpec;
27  import javax.servlet.http.HttpServletRequest;
28  import javax.servlet.http.HttpServletResponse;
29  
30  import net.sourceforge.statelessfilter.backend.ISessionData;
31  import net.sourceforge.statelessfilter.backend.support.CookieBackendSupport;
32  import net.sourceforge.statelessfilter.backend.support.CookieDataSupport;
33  
34  import org.apache.commons.codec.binary.Base64;
35  import org.apache.commons.lang.ArrayUtils;
36  import org.apache.commons.lang.StringUtils;
37  import org.codehaus.jackson.map.ObjectMapper;
38  import org.slf4j.Logger;
39  import org.slf4j.LoggerFactory;
40  
41  /**
42   * Backend based on an AES-encrypted cookie. Cookie can be compressed to save
43   * bandwidth.
44   * 
45   * <p>
46   * Parameters :
47   * </p>
48   * <ul>
49   * <li>cookiename : name of the cookie</li>
50   * <li>compress : enable cookie compression (gzip) when value is "true"</li>
51   * <li>key : key used to encrypt data</li>
52   * <li>iv : iv used to encrypt data</li>
53   * </ul>
54   * 
55   * @author Nicolas Richeton - Capgemini
56   * 
57   */
58  public class JSONAESCookieBackend extends CookieBackendSupport {
59  	private static final String DESERIALIZE_ERROR = "Cannot deserialize session. A new one will be created"; //$NON-NLS-1$
60  	private static final String ENCRYPTION = "AES"; //$NON-NLS-1$
61  	private static final String ENCRYPTION_WITH_PARAM = "AES/CBC/PKCS5Padding"; //$NON-NLS-1$
62  	private static final String ID = "jsonaescookie"; //$NON-NLS-1$
63  	private static final String PARAM_IV = "iv"; //$NON-NLS-1$
64  	private static final String PARAM_KEY = "key"; //$NON-NLS-1$
65  	private static final String SEPARATOR = "B"; //$NON-NLS-1$
66  
67  	private IvParameterSpec iv = null;
68  	Logger logger = LoggerFactory.getLogger(JSONAESCookieBackend.class);
69  	ObjectMapper mapper = null;
70  	private SecretKeySpec secretKey = null;
71  
72  	public JSONAESCookieBackend() {
73  		setCookieName("es"); //$NON-NLS-1$
74  	}
75  
76  	/**
77  	 * @see com.capgemini.stateless.backend.plaincookie.ISessionBackend#destroy()
78  	 */
79  	@Override
80  	public void destroy() {
81  		// Nothing to do
82  	}
83  
84  	private byte[] getEncryptionBytes(String data, int length) {
85  		byte[] keyRaw = new byte[length];
86  		for (int i = 0; i < length; i++) {
87  			keyRaw[i] = 0;
88  		}
89  
90  		byte[] dataRaw = Base64.decodeBase64(data);
91  		System.arraycopy(dataRaw, 0, keyRaw, 0,
92  				dataRaw.length > length ? length : dataRaw.length);
93  
94  		return keyRaw;
95  	}
96  
97  	/**
98  	 * @see com.capgemini.stateless.backend.plaincookie.ISessionBackend#getId()
99  	 */
100 	@Override
101 	public String getId() {
102 		return ID;
103 	}
104 
105 	/**
106 	 * Loads key and iv for encryption and performs normal init.
107 	 * 
108 	 * @throws Exception
109 	 * @see com.capgemini.stateless.backend.plaincookie.ISessionBackend#init(java.util.Map)
110 	 */
111 	@Override
112 	public void init(Map<String, String> config) throws Exception {
113 		super.init(config);
114 		mapper = new ObjectMapper();
115 		String key = config.get(PARAM_KEY);
116 		String iv = config.get(PARAM_IV);
117 
118 		if (StringUtils.isEmpty(key) || StringUtils.isEmpty(iv)) {
119 			throw new Exception(
120 					ID
121 							+ "."
122 							+ PARAM_KEY
123 							+ " or "
124 							+ ID
125 							+ "." + PARAM_IV + " parameter missing in /stateless.properties."); //$NON-NLS-1$
126 		}
127 
128 		secretKey = new SecretKeySpec(getEncryptionBytes(key, 16), ENCRYPTION);
129 		this.iv = new IvParameterSpec(getEncryptionBytes(iv, 16));
130 
131 	}
132 
133 	/**
134 	 * @see com.capgemini.stateless.backend.plaincookie.ISessionBackend#restore(javax.servlet.http.HttpServletRequest)
135 	 */
136 	@Override
137 	public ISessionData restore(HttpServletRequest request) {
138 		try {
139 
140 			byte[] data = getCookieData(request, null);
141 
142 			if (data != null) {
143 				int index = ArrayUtils.indexOf(data, SEPARATOR.getBytes()[0]);
144 
145 				int size = Integer.parseInt(new String(ArrayUtils.subarray(
146 						data, 0, index)));
147 				data = ArrayUtils.subarray(data, index + 1, data.length);
148 
149 				Cipher decryptCipher = Cipher
150 						.getInstance(ENCRYPTION_WITH_PARAM);
151 				decryptCipher.init(Cipher.DECRYPT_MODE, secretKey, iv);
152 				data = decryptCipher.doFinal(data);
153 
154 				data = ArrayUtils.subarray(data, 0, size + 1);
155 
156 				CookieDataSupport s = mapper.readValue(new String(data),
157 						CookieDataSupport.class);
158 
159 				if (s.isValid()
160 						&& s.getRemoteAddress().equals(
161 								getFullRemoteAddr(request))) {
162 					return s;
163 				}
164 			}
165 		} catch (Exception e) {
166 			logger.info(DESERIALIZE_ERROR, e);
167 		}
168 
169 		return null;
170 	}
171 
172 	/**
173 	 * @see net.sourceforge.statelessfilter.backend.support.CookieBackendSupport#save(net.sourceforge.statelessfilter.backend.ISessionData,
174 	 *      java.util.List, javax.servlet.http.HttpServletRequest,
175 	 *      javax.servlet.http.HttpServletResponse)
176 	 */
177 	@Override
178 	public void save(ISessionData session, List<String> dirtyAttributes,
179 			HttpServletRequest request, HttpServletResponse response)
180 			throws IOException {
181 		try {
182 			if (session != null) {
183 				CookieDataSupport cookieData = new CookieDataSupport(session);
184 				cookieData.setRemoteAddress(getFullRemoteAddr(request));
185 
186 				// JSON only supports Strings.
187 				ensureStrings(session.getContent());
188 
189 				String dataString = mapper.writeValueAsString(cookieData);
190 
191 				byte[] data;
192 				try {
193 					Cipher encryptCipher = Cipher
194 							.getInstance(ENCRYPTION_WITH_PARAM);
195 					encryptCipher.init(Cipher.ENCRYPT_MODE, secretKey, iv);
196 					data = encryptCipher.doFinal(dataString.getBytes());
197 				} catch (Exception e) {
198 					throw new IOException(e.getMessage());
199 				}
200 
201 				byte[] size = (data.length + SEPARATOR).getBytes();
202 
203 				setCookieData(request, response, ArrayUtils.addAll(size, data));
204 
205 				if (logger.isDebugEnabled()) {
206 					logger.debug("Cookie size : " + ArrayUtils.addAll(size, data).length); //$NON-NLS-1$
207 				}
208 
209 			} else {
210 				setCookieData(request, response, null);
211 			}
212 		} catch (SignatureException e) {
213 			throw new IOException(e);
214 		}
215 	}
216 
217 	/**
218 	 * Throw IllegalArgumentException if the map does not contains only String
219 	 * objects.
220 	 * 
221 	 * @param map
222 	 */
223 	private void ensureStrings(Map<String, Object> map) {
224 
225 		if (map == null || map.size() == 0)
226 			return;
227 
228 		Set<String> keys = map.keySet();
229 		for (String key : keys) {
230 			if (!(map.get(key) instanceof String)) {
231 				throw new IllegalArgumentException(
232 						key
233 								+ " is not a String. JSON stateless session only support string data.");
234 			}
235 		}
236 	}
237 }