001 // Copyright 2009, 2012 The Apache Software Foundation
002 //
003 // Licensed under the Apache License, Version 2.0 (the "License");
004 // you may not use this file except in compliance with the License.
005 // You may obtain a copy of the License at
006 //
007 // http://www.apache.org/licenses/LICENSE-2.0
008 //
009 // Unless required by applicable law or agreed to in writing, software
010 // distributed under the License is distributed on an "AS IS" BASIS,
011 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
012 // See the License for the specific language governing permissions and
013 // limitations under the License.
014
015 package org.apache.tapestry5.internal.services;
016
017 import org.apache.tapestry5.SymbolConstants;
018 import org.apache.tapestry5.alerts.AlertManager;
019 import org.apache.tapestry5.internal.InternalConstants;
020 import org.apache.tapestry5.internal.TapestryInternalUtils;
021 import org.apache.tapestry5.internal.util.Base64InputStream;
022 import org.apache.tapestry5.internal.util.MacOutputStream;
023 import org.apache.tapestry5.ioc.annotations.Symbol;
024 import org.apache.tapestry5.services.ClientDataEncoder;
025 import org.apache.tapestry5.services.ClientDataSink;
026 import org.apache.tapestry5.services.URLEncoder;
027 import org.slf4j.Logger;
028
029 import javax.crypto.spec.SecretKeySpec;
030 import java.io.BufferedInputStream;
031 import java.io.IOException;
032 import java.io.ObjectInputStream;
033 import java.io.UnsupportedEncodingException;
034 import java.security.Key;
035 import java.util.zip.GZIPInputStream;
036
037 public class ClientDataEncoderImpl implements ClientDataEncoder
038 {
039 private final URLEncoder urlEncoder;
040
041 private final Key hmacKey;
042
043 public ClientDataEncoderImpl(URLEncoder urlEncoder, @Symbol(SymbolConstants.HMAC_PASSPHRASE) String passphrase,
044 Logger logger,
045 @Symbol(InternalConstants.TAPESTRY_APP_PACKAGE_PARAM)
046 String applicationPackageName, AlertManager alertManager) throws UnsupportedEncodingException
047 {
048 this.urlEncoder = urlEncoder;
049
050 if (passphrase.equals(""))
051 {
052 String message = String.format("The symbol '%s' has not been configured. " +
053 "This is used to configure hash-based message authentication of Tapestry data stored in forms, or in the URL. " +
054 "You application is less secure, and more vulnerable to denial-of-service attacks, when this symbol is not configured.",
055 SymbolConstants.HMAC_PASSPHRASE);
056
057 // Now to really get the attention of the developer!
058
059 alertManager.error(message);
060
061 logger.error(message);
062
063 // Override the blank parameter to set a default value. Use the application package name,
064 // which is justly slightly more secure than having a fixed default.
065 passphrase = applicationPackageName;
066 }
067
068 hmacKey = new SecretKeySpec(passphrase.getBytes("UTF8"), "HmacSHA1");
069 }
070
071 public ClientDataSink createSink()
072 {
073 try
074 {
075 return new ClientDataSinkImpl(urlEncoder, hmacKey);
076 } catch (IOException ex)
077 {
078 throw new RuntimeException(ex);
079 }
080 }
081
082 public ObjectInputStream decodeClientData(String clientData)
083 {
084 // The clientData is Base64 that's been gzip'ed (i.e., this matches
085 // what ClientDataSinkImpl does).
086
087 int colonx = clientData.indexOf(':');
088
089 if (colonx < 0)
090 {
091 throw new IllegalArgumentException("Client data must be prefixed with its HMAC code.");
092 }
093
094 // Extract the string presumably encoded by the server using the secret key.
095
096 String storedHmacResult = clientData.substring(0, colonx);
097
098 String clientStream = clientData.substring(colonx + 1);
099
100 try
101 {
102 Base64InputStream b64in = new Base64InputStream(clientStream);
103
104 validateHMAC(storedHmacResult, b64in);
105
106 // After reading it once to validate, reset it for the actual read (which includes the GZip decompression).
107
108 b64in.reset();
109
110 BufferedInputStream buffered = new BufferedInputStream(new GZIPInputStream(b64in));
111
112 return new ObjectInputStream(buffered);
113 } catch (IOException ex)
114 {
115 throw new RuntimeException(ex);
116 }
117 }
118
119 private void validateHMAC(String storedHmacResult, Base64InputStream b64in) throws IOException
120 {
121 MacOutputStream macOs = MacOutputStream.streamFor(hmacKey);
122
123 TapestryInternalUtils.copy(b64in, macOs);
124
125 macOs.close();
126
127 String actual = macOs.getResult();
128
129 if (!storedHmacResult.equals(actual))
130 {
131 throw new IOException("Client data associated with the current request appears to have been tampered with " +
132 "(the HMAC signature does not match).");
133 }
134 }
135
136 public ObjectInputStream decodeEncodedClientData(String clientData) throws IOException
137 {
138 return decodeClientData(urlEncoder.decode(clientData));
139 }
140 }