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.aescookie;
17  
18  import static org.apache.commons.lang.StringUtils.defaultIfEmpty;
19  import static org.apache.commons.lang.StringUtils.isEmpty;
20  import static org.apache.commons.lang.StringUtils.trim;
21  
22  import java.io.ByteArrayInputStream;
23  import java.io.ByteArrayOutputStream;
24  import java.io.IOException;
25  import java.io.InputStream;
26  import java.io.ObjectInputStream;
27  import java.io.ObjectOutputStream;
28  import java.io.OutputStream;
29  import java.security.SignatureException;
30  import java.util.List;
31  import java.util.Map;
32  import java.util.UUID;
33  import java.util.zip.GZIPInputStream;
34  import java.util.zip.GZIPOutputStream;
35  
36  import javax.crypto.Cipher;
37  import javax.crypto.spec.IvParameterSpec;
38  import javax.crypto.spec.SecretKeySpec;
39  import javax.servlet.http.HttpServletRequest;
40  import javax.servlet.http.HttpServletResponse;
41  
42  import net.sourceforge.statelessfilter.backend.ISessionData;
43  import net.sourceforge.statelessfilter.backend.support.CookieBackendSupport;
44  import net.sourceforge.statelessfilter.backend.support.CookieDataSupport;
45  
46  import org.apache.commons.codec.binary.Base64;
47  import org.apache.commons.lang.ArrayUtils;
48  import org.apache.commons.lang.StringUtils;
49  import org.slf4j.Logger;
50  import org.slf4j.LoggerFactory;
51  
52  /**
53   * Backend based on an AES-encrypted cookie. Cookie can be compressed to save
54   * bandwidth.
55   * 
56   * <p>
57   * Parameters :
58   * </p>
59   * <ul>
60   * <li>cookiename : name of the cookie</li>
61   * <li>compress : enable cookie compression (gzip) when value is "true"</li>
62   * <li>key : key used to encrypt data</li>
63   * <li>iv : iv used to encrypt data</li>
64   * </ul>
65   * 
66   * @author Nicolas Richeton - Capgemini
67   * 
68   */
69  public class AESCookieBackend extends CookieBackendSupport {
70      private static final String DESERIALIZE_ERROR = "Cannot deserialize session. A new one will be created"; //$NON-NLS-1$
71      private static final String ENCRYPTION = "AES"; //$NON-NLS-1$
72      private static final String ENCRYPTION_WITH_PARAM = "AES/CBC/PKCS5Padding"; //$NON-NLS-1$
73      private static final String ID = "aescookie"; //$NON-NLS-1$
74      private static Logger logger = LoggerFactory.getLogger(AESCookieBackend.class);
75      public static final String PARAM_COMPRESS = "compress"; //$NON-NLS-1$
76      public static final String PARAM_IV = "iv"; //$NON-NLS-1$
77      public static final String PARAM_KEY = "key"; //$NON-NLS-1$
78      public static final String PARAM_RESTRICT_IP = "restrictIP";
79      public static final String PARAM_SESSION_MAX_TIME = "sessionMaxTime";
80      public static final String PARAM_SIGN_SECRET = "secret";
81      private static final String SEPARATOR = "B"; //$NON-NLS-1$
82      private boolean compress = true;
83      private IvParameterSpec iv = null;
84      private boolean restrictIp;
85      private SecretKeySpec secretKey = null;
86      private Integer sessionMaxTime; // seconds
87      private String signSecret;
88  
89      public AESCookieBackend() {
90          setCookieName("es"); //$NON-NLS-1$
91      }
92  
93      /**
94       * @see com.capgemini.stateless.backend.plaincookie.ISessionBackend#destroy()
95       */
96      @Override
97      public void destroy() {
98          // Nothing to do
99      }
100 
101     public boolean getCompress() {
102         return compress;
103     }
104 
105     private byte[] getEncryptionBytes(String data, int length) {
106         byte[] keyRaw = new byte[length];
107         for (int i = 0; i < length; i++) {
108             keyRaw[i] = 0;
109         }
110 
111         byte[] dataRaw = Base64.decodeBase64(data);
112         System.arraycopy(dataRaw, 0, keyRaw, 0, dataRaw.length > length ? length : dataRaw.length);
113 
114         return keyRaw;
115     }
116 
117     /**
118      * @see com.capgemini.stateless.backend.plaincookie.ISessionBackend#getId()
119      */
120     @Override
121     public String getId() {
122         return ID;
123     }
124 
125     public boolean getRestrictIp() {
126         return restrictIp;
127     }
128 
129     public Integer getSessionMaxTime() {
130         return sessionMaxTime;
131     }
132 
133     public String getSignSecret() {
134         return signSecret;
135     }
136 
137     /**
138      * Loads key and iv for encryption and performs normal init.
139      * 
140      * @throws Exception
141      * @see com.capgemini.stateless.backend.plaincookie.ISessionBackend#init(java.util.Map)
142      */
143     @Override
144     public void init(Map<String, String> config) throws Exception {
145         super.init(config);
146         this.compress = Boolean.parseBoolean(defaultIfEmpty(trim(config.get(PARAM_COMPRESS)), "true"));
147         this.restrictIp = Boolean.parseBoolean(defaultIfEmpty(trim(config.get(PARAM_RESTRICT_IP)), "true"));
148         this.signSecret = defaultIfEmpty(config.get(PARAM_SIGN_SECRET), UUID.randomUUID().toString());
149         this.sessionMaxTime = isEmpty(config.get(PARAM_SESSION_MAX_TIME)) ? null : Integer.parseInt(trim(config
150                 .get(PARAM_SESSION_MAX_TIME)));
151         if (logger.isInfoEnabled()) {
152             logger.info(
153                     "Cookie name: '{}', compression: '{}', " + "session max time: '{}', restrict IP: '{}'", new Object[] { this.cookieName, this.compress, this.sessionMaxTime, this.restrictIp }); //$NON-NLS-1$
154         }
155 
156         // AES configuration
157         String key = config.get(PARAM_KEY);
158         String iv = config.get(PARAM_IV);
159 
160         if (isEmpty(key) || isEmpty(iv)) {
161             throw new IllegalArgumentException(ID
162                     + "." + PARAM_KEY + " or " + ID + "." + PARAM_IV + " parameter missing in /stateless.properties."); //$NON-NLS-1$
163         }
164 
165         secretKey = new SecretKeySpec(getEncryptionBytes(key, 16), ENCRYPTION);
166         this.iv = new IvParameterSpec(getEncryptionBytes(iv, 16));
167 
168     }
169 
170     /**
171      * @see com.capgemini.stateless.backend.plaincookie.ISessionBackend#restore(javax.servlet.http.HttpServletRequest)
172      */
173     @Override
174     public ISessionData restore(HttpServletRequest request) {
175 
176         try {
177             byte[] data = getCookieData(request, null, true, this.signSecret);
178             if (data != null) {
179                 int index = ArrayUtils.indexOf(data, SEPARATOR.getBytes()[0]);
180 
181                 int size = Integer.parseInt(new String(ArrayUtils.subarray(data, 0, index)));
182                 data = ArrayUtils.subarray(data, index + 1, data.length);
183 
184                 Cipher decryptCipher = Cipher.getInstance(ENCRYPTION_WITH_PARAM);
185                 decryptCipher.init(Cipher.DECRYPT_MODE, secretKey, iv);
186                 data = decryptCipher.doFinal(data);
187 
188                 data = ArrayUtils.subarray(data, 0, size + 1);
189 
190                 InputStream inputStream = new ByteArrayInputStream(data);
191                 if (compress) {
192                     inputStream = new GZIPInputStream(inputStream);
193                 }
194 
195                 ObjectInputStream ois = new ObjectInputStream(inputStream);
196                 CookieDataSupport s = (CookieDataSupport) ois.readObject();
197 
198                 if (restrictIp
199                         && (!StringUtils.equals(s.getRemoteAddress(), getFullRemoteAddr(request)) || StringUtils
200                                 .isEmpty(s.getRemoteAddress()))) {
201                     s.setValid(false);
202                     logger.warn("Invalid IP. Expected: " + s.getRemoteAddress() + ", current: "
203                             + getFullRemoteAddr(request));
204                 }
205 
206                 if (sessionMaxTime != null
207                         && System.currentTimeMillis() > s.getCreationTime() + sessionMaxTime.intValue() * 1000) {
208                     s.setValid(false);
209                     logger.info("Session max time reached.");
210                 }
211 
212                 if (s.isValid()) {
213                     return s;
214                 }
215             }
216         } catch (Exception e) {
217             logger.info(DESERIALIZE_ERROR, e);
218         }
219 
220         return null;
221     }
222 
223     /**
224      * @see net.sourceforge.statelessfilter.backend.support.CookieBackendSupport#save(net.sourceforge.statelessfilter.backend.ISessionData,
225      *      java.util.List, javax.servlet.http.HttpServletRequest,
226      *      javax.servlet.http.HttpServletResponse)
227      */
228     @Override
229     public void save(ISessionData session, List<String> dirtyAttributes, HttpServletRequest request,
230             HttpServletResponse response) throws IOException {
231 
232         try {
233             if (session != null) {
234                 CookieDataSupport cookieData = new CookieDataSupport(session);
235                 cookieData.setRemoteAddress(getFullRemoteAddr(request));
236 
237                 ByteArrayOutputStream baos = new ByteArrayOutputStream();
238                 OutputStream outputStream = baos;
239                 if (compress) {
240                     outputStream = new GZIPOutputStream(outputStream);
241                 }
242 
243                 ObjectOutputStream oos = new ObjectOutputStream(outputStream);
244                 oos.writeObject(cookieData);
245                 oos.close();
246                 outputStream.close();
247                 baos.close();
248 
249                 byte[] data;
250                 try {
251                     Cipher encryptCipher = Cipher.getInstance(ENCRYPTION_WITH_PARAM);
252                     encryptCipher.init(Cipher.ENCRYPT_MODE, secretKey, iv);
253                     data = encryptCipher.doFinal(baos.toByteArray());
254                 } catch (Exception e) {
255                     throw new IOException(e.getMessage());
256                 }
257 
258                 byte[] size = (data.length + SEPARATOR).getBytes();
259 
260                 setCookieData(request, response, ArrayUtils.addAll(size, data), true, this.signSecret);
261 
262                 if (logger.isDebugEnabled()) {
263                     logger.debug("Cookie size : " + ArrayUtils.addAll(size, data).length); //$NON-NLS-1$
264                 }
265 
266             } else {
267                 setCookieData(request, response, null, true, this.signSecret);
268             }
269         } catch (SignatureException e) {
270             throw new IOException(e);
271         }
272     }
273 }